diff --git a/nuget/build-appveyor.ps1 b/nuget/build-appveyor.ps1 index d74216def0..5d7f5f1628 100644 --- a/nuget/build-appveyor.ps1 +++ b/nuget/build-appveyor.ps1 @@ -9,11 +9,8 @@ sv version $env:APPVEYOR_BUILD_NUMBER sv version 9999.0.$version-nightly sv key $env:myget_key -sv file Perspex.$version.nupkg - .\build-version.ps1 $version - sv reponame $env:APPVEYOR_REPO_NAME sv repobranch $env:APPVEYOR_REPO_BRANCH sv pullreq $env:APPVEYOR_PULL_REQUEST_NUMBER @@ -26,7 +23,8 @@ if ($reponame -eq "Perspex/Perspex") if($repobranch -eq "master") { echo Repo branch matched - nuget.exe push $file $key -Source https://www.myget.org/F/perspex-nightly/api/v2/package + nuget.exe push Perspex.$version.nupkg $key -Source https://www.myget.org/F/perspex-nightly/api/v2/package + nuget.exe push Perspex.Desktop.$version.nupkg $key -Source https://www.myget.org/F/perspex-nightly/api/v2/package } } diff --git a/nuget/build-version.ps1 b/nuget/build-version.ps1 index f458b30ffd..2c7e15683a 100644 --- a/nuget/build-version.ps1 +++ b/nuget/build-version.ps1 @@ -2,7 +2,8 @@ rm -Force -Recurse .\Perspex -ErrorAction SilentlyContinue rm -Force -Recurse *.nupkg -ErrorAction SilentlyContinue Copy-Item template Perspex -Recurse sv lib "Perspex\lib\portable-windows8+net45" -sv build "Perspex\build\net45" +sv build "Perspex.Desktop\lib\net45" + mkdir $lib -ErrorAction SilentlyContinue mkdir $build -ErrorAction SilentlyContinue @@ -36,14 +37,14 @@ Copy-Item ..\src\Perspex.HtmlRenderer\bin\Release\Perspex.HtmlRenderer.dll $lib Copy-Item ..\src\Perspex.ReactiveUI\bin\Release\Perspex.ReactiveUI.dll $lib Copy-Item ..\src\Windows\Perspex.Direct2D1\bin\Release\Perspex.Direct2D1.dll $build -Copy-Item ..\src\Windows\Perspex.Direct2D1\bin\Release\SharpDX.dll $build -Copy-Item ..\src\Windows\Perspex.Direct2D1\bin\Release\SharpDX.Direct2D1.dll $build -Copy-Item ..\src\Windows\Perspex.Direct2D1\bin\Release\SharpDX.DXGI.dll $build Copy-Item ..\src\Windows\Perspex.Win32\bin\Release\Perspex.Win32.dll $build Copy-Item ..\src\Gtk\Perspex.Gtk\bin\Release\Perspex.Gtk.dll $build Copy-Item ..\src\Gtk\Perspex.Cairo\bin\Release\Perspex.Cairo.dll $build (gc Perspex\Perspex.nuspec).replace('#VERSION#', $args[0]) | sc Perspex\Perspex.nuspec +(gc Perspex\Perspex.Desktop.nuspec).replace('#VERSION#', $args[0]) | sc Perspex.Desktop\Perspex.Desktop.nuspec nuget.exe pack Perspex\Perspex.nuspec -rm -Force -Recurse .\Perspex \ No newline at end of file +nuget.exe pack Perspex.Desktop\Perspex.Desktop.nuspec +rm -Force -Recurse .\Perspex +rm -Force -Recurse .\Perspex.Desktop \ No newline at end of file diff --git a/nuget/template/Perspex.Desktop.nuspec b/nuget/template/Perspex.Desktop.nuspec new file mode 100644 index 0000000000..b94e9c54f0 --- /dev/null +++ b/nuget/template/Perspex.Desktop.nuspec @@ -0,0 +1,29 @@ + + + + Perspex.Desktop + #VERSION# + Perspex Team + stevenk + http://opensource.org/licenses/MIT + https://github.com/Perspex/Perspex/ + false + The Perspex UI framework + + Copyright 2015 + Perspex + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nuget/template/Perspex.nuspec b/nuget/template/Perspex.nuspec index 45490d310e..7c55e7aaf6 100644 --- a/nuget/template/Perspex.nuspec +++ b/nuget/template/Perspex.nuspec @@ -3,10 +3,10 @@ Perspex #VERSION# - stevenk + Perspex Team stevenk http://opensource.org/licenses/MIT - https://github.com/grokys/Perspex/ + https://github.com/Perspex/Perspex/ false The Perspex UI framework Initial alpha release. @@ -21,6 +21,7 @@ + \ No newline at end of file diff --git a/nuget/template/build/net45/perspex.targets b/nuget/template/build/net45/perspex.targets deleted file mode 100644 index 70d36cf835..0000000000 --- a/nuget/template/build/net45/perspex.targets +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - %(RecursiveDir)%(FileName)%(Extension) - PreserveNewest - - - \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/XamlTemplateBinding.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlTemplateBinding.cs index 4ccb6ff903..84794386a6 100644 --- a/src/Markup/Perspex.Markup.Xaml/Binding/XamlTemplateBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlTemplateBinding.cs @@ -40,7 +40,7 @@ namespace Perspex.Markup.Xaml.Binding PerspexProperty targetProperty, PerspexObject templatedParent) { - var sourceProperty = templatedParent.FindRegistered(SourcePropertyPath); + var sourceProperty = PerspexPropertyRegistry.Instance.FindRegistered(instance.GetType(), SourcePropertyPath); if (sourceProperty == null) { diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index 7053eb3e95..280bcec5a6 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs @@ -88,7 +88,7 @@ namespace Perspex.Markup.Xaml.Context if (attached == null) { propertyName = _xamlMember.Name; - property = perspexObject.GetRegisteredProperties() + property = PerspexPropertyRegistry.Instance.GetRegistered(perspexObject) .FirstOrDefault(x => x.Name == propertyName); } else @@ -98,7 +98,7 @@ namespace Perspex.Markup.Xaml.Context propertyName = attached.DeclaringType.UnderlyingType.Name + '.' + _xamlMember.Name; - property = perspexObject.GetRegisteredProperties() + property = PerspexPropertyRegistry.Instance.GetRegistered(perspexObject) .Where(x => x.IsAttached && x.OwnerType == attached.DeclaringType.UnderlyingType) .FirstOrDefault(x => x.Name == _xamlMember.Name); } diff --git a/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs b/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs index f492e01111..1cdc3cc4f1 100644 --- a/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs +++ b/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs @@ -46,12 +46,12 @@ namespace Perspex.Markup.Xaml.Converters } // First look for non-attached property on the type and then look for an attached property. - var property = PerspexObject.GetRegisteredProperties(type) + var property = PerspexPropertyRegistry.Instance.GetRegistered(type) .FirstOrDefault(x => x.Name == propertyName); if (property == null) { - property = PerspexObject.GetAttachedProperties(type) + property = PerspexPropertyRegistry.Instance.GetAttached(type) .FirstOrDefault(x => x.Name == propertyName); } diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index b7227d831b..5b3a086f59 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -50,6 +50,7 @@ + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 337fbf185d..a61e04a8ac 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -25,18 +25,6 @@ namespace Perspex /// public class PerspexObject : IObservablePropertyBag, INotifyPropertyChanged { - /// - /// The registered properties by type. - /// - private static readonly Dictionary> s_registered = - new Dictionary>(); - - /// - /// The registered attached properties by owner type. - /// - private static readonly Dictionary> s_attached = - new Dictionary>(); - /// /// The parent object that inherited values are inherited from. /// @@ -70,7 +58,7 @@ namespace Perspex new PropertyEnricher("Id", GetHashCode()), }); - foreach (var property in GetRegisteredProperties()) + foreach (var property in PerspexPropertyRegistry.Instance.GetRegistered(this)) { object value = property.IsDirect ? property.Getter(this) : @@ -129,7 +117,7 @@ namespace Perspex _inheritanceParent.PropertyChanged -= ParentPropertyChanged; } - var inherited = (from property in GetRegisteredProperties(GetType()) + var inherited = (from property in PerspexPropertyRegistry.Instance.GetRegistered(this) where property.Inherits select new { @@ -215,92 +203,6 @@ namespace Perspex } } - /// - /// Gets all s registered on a type. - /// - /// The type. - /// A collection of definitions. - public static IEnumerable GetRegisteredProperties(Type type) - { - Contract.Requires(type != null); - - TypeInfo i = type.GetTypeInfo(); - - while (type != null) - { - List list; - - if (s_registered.TryGetValue(type, out list)) - { - foreach (PerspexProperty p in list) - { - yield return p; - } - } - - type = type.GetTypeInfo().BaseType; - } - } - - /// - /// Gets all attached s registered by an owner. - /// - /// The owner type. - /// A collection of definitions. - public static IEnumerable GetAttachedProperties(Type ownerType) - { - List list; - - if (s_attached.TryGetValue(ownerType, out list)) - { - return list; - } - - return Enumerable.Empty(); - } - - /// - /// Registers a on a type. - /// - /// The type. - /// The property. - /// - /// You won't usually want to call this method directly, instead use the - /// method. - /// - public static void Register(Type type, PerspexProperty property) - { - Contract.Requires(type != null); - Contract.Requires(property != null); - - List list; - - if (!s_registered.TryGetValue(type, out list)) - { - list = new List(); - s_registered.Add(type, list); - } - - if (!list.Contains(property)) - { - list.Add(property); - } - - if (property.IsAttached) - { - if (!s_attached.TryGetValue(property.OwnerType, out list)) - { - list = new List(); - s_attached.Add(property.OwnerType, list); - } - - if (!list.Contains(property)) - { - list.Add(property); - } - } - } - public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); @@ -409,7 +311,7 @@ namespace Perspex object result = PerspexProperty.UnsetValue; PriorityValue value; - if (!IsRegistered(property)) + if (!PerspexPropertyRegistry.Instance.IsRegistered(this, property)) { ThrowNotRegistered(property); } @@ -448,17 +350,6 @@ namespace Perspex } } - /// - /// Gets all properties that are registered on this object. - /// - /// - /// A collection of objects. - /// - public IEnumerable GetRegisteredProperties() - { - return GetRegisteredProperties(GetType()); - } - /// /// Checks whether a is set on this object. /// @@ -478,16 +369,6 @@ namespace Perspex return false; } - /// - /// Checks whether a is registered on this class. - /// - /// The property. - /// True if the property is registered, otherwise false. - public bool IsRegistered(PerspexProperty property) - { - return FindRegistered(property) != null; - } - /// /// Sets a value. /// @@ -519,7 +400,7 @@ namespace Perspex PriorityValue v; var originalValue = value; - if (!IsRegistered(property)) + if (!PerspexPropertyRegistry.Instance.IsRegistered(this, property)) { ThrowNotRegistered(property); } @@ -620,7 +501,7 @@ namespace Perspex { PriorityValue v; - if (!IsRegistered(property)) + if (!PerspexPropertyRegistry.Instance.IsRegistered(this, property)) { ThrowNotRegistered(property); } @@ -751,6 +632,12 @@ namespace Perspex } } + /// + bool IPropertyBag.IsRegistered(PerspexProperty property) + { + return PerspexPropertyRegistry.Instance.IsRegistered(this, property); + } + /// /// Gets all priority values set on the object. /// @@ -936,41 +823,6 @@ namespace Perspex } } - /// - /// Given a returns a registered perspex property that is - /// equal. - /// - /// The property. - /// The registered property or null if not found. - /// - /// Calling AddOwner on a direct PerspexProperty creates new new PerspexProperty with - /// an overridden getter and setter. This property is a different object but is equal - /// according to . - /// - public PerspexProperty FindRegistered(PerspexProperty property) - { - Type type = GetType(); - - while (type != null) - { - List list; - - if (s_registered.TryGetValue(type, out list)) - { - var index = list.IndexOf(property); - - if (index != -1) - { - return list[index]; - } - } - - type = type.GetTypeInfo().BaseType; - } - - return null; - } - /// /// Given a returns a registered perspex property that is /// equal or throws if not found. @@ -979,7 +831,7 @@ namespace Perspex /// The registered property. public PerspexProperty GetRegistered(PerspexProperty property) { - var result = FindRegistered(property); + var result = PerspexPropertyRegistry.Instance.FindRegistered(this, property); if (result == null) { diff --git a/src/Perspex.Base/PerspexObjectExtensions.cs b/src/Perspex.Base/PerspexObjectExtensions.cs index 6aad57bb2c..7ba3fd33d4 100644 --- a/src/Perspex.Base/PerspexObjectExtensions.cs +++ b/src/Perspex.Base/PerspexObjectExtensions.cs @@ -52,50 +52,6 @@ namespace Perspex return observable.Subscribe(e => SubscribeAdapter(e, handler)); } - /// - /// Finds a registered property on a by name. - /// - /// The object. - /// - /// The property name. If an attached property it should be in the form - /// "OwnerType.PropertyName". - /// - /// - /// The registered property or null if no matching property found. - /// - public static PerspexProperty FindRegistered(this PerspexObject o, string name) - { - Contract.Requires(o != null); - Contract.Requires(name != null); - - var parts = name.Split('.'); - - if (parts.Length < 1 || parts.Length > 2) - { - throw new ArgumentException("Invalid property name."); - } - - if (parts.Length == 1) - { - var result = o.GetRegisteredProperties() - .FirstOrDefault(x => !x.IsAttached && x.Name == parts[0]); - - if (result != null) - { - return result; - } - - // A type can .AddOwner an attached property. - return o.GetRegisteredProperties() - .FirstOrDefault(x => x.Name == parts[0]); - } - else - { - return o.GetRegisteredProperties() - .FirstOrDefault(x => x.IsAttached && x.OwnerType.Name == parts[0] && x.Name == parts[1]); - } - } - /// /// Observer method for . diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index e44284b570..2dd679ce8d 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Subjects; using System.Reflection; using Perspex.Utilities; @@ -28,7 +29,12 @@ namespace Perspex private static int s_nextId = 1; /// - /// The default values for the property, by type. + /// The default value provided when the property was first registered. + /// + private readonly object _defaultValue; + + /// + /// The overridden default values for the property, by type. /// private readonly Dictionary _defaultValues = new Dictionary(); @@ -92,7 +98,7 @@ namespace Perspex Name = name; PropertyType = valueType; OwnerType = ownerType; - _defaultValues.Add(ownerType, defaultValue); + _defaultValue = defaultValue; Inherits = inherits; DefaultBindingMode = defaultBindingMode; IsAttached = isAttached; @@ -143,14 +149,57 @@ namespace Perspex /// Initializes a new instance of the class. /// /// The direct property to copy. + /// The new owner type. + protected PerspexProperty(PerspexProperty source, Type ownerType) + { + Contract.Requires(source != null); + Contract.Requires(ownerType != null); + + if (source.IsDirect) + { + throw new InvalidOperationException( + "This method cannot be called on direct PerspexProperties."); + } + + //Name = name; + //PropertyType = valueType; + //OwnerType = ownerType; + //_defaultValues.Add(ownerType, defaultValue); + //Inherits = inherits; + //DefaultBindingMode = defaultBindingMode; + //IsAttached = isAttached; + //Notifying = notifying; + //_id = s_nextId++; + + + Name = source.Name; + PropertyType = source.PropertyType; + OwnerType = ownerType; + _defaultValue = source._defaultValue; + _defaultValues = source._defaultValues; + Inherits = source.Inherits; + DefaultBindingMode = source.DefaultBindingMode; + IsAttached = false; + Notifying = Notifying; + _validation = source._validation; + _id = source._id; + } + + /// + /// Initializes a new instance of the class. + /// + /// The direct property to copy. + /// The new owner type. /// A new getter. /// A new setter. protected PerspexProperty( PerspexProperty source, + Type ownerType, Func getter, Action setter) { Contract.Requires(source != null); + Contract.Requires(ownerType != null); Contract.Requires(getter != null); if (!source.IsDirect) @@ -161,7 +210,7 @@ namespace Perspex Name = source.Name; PropertyType = source.PropertyType; - OwnerType = source.OwnerType; + OwnerType = ownerType; Getter = getter; Setter = setter; IsDirect = true; @@ -367,7 +416,7 @@ namespace Perspex notifying, false); - PerspexObject.Register(typeof(TOwner), result); + PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } @@ -395,7 +444,7 @@ namespace Perspex Cast(getter), Cast(setter)); - PerspexObject.Register(typeof(TOwner), result); + PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } @@ -431,7 +480,7 @@ namespace Perspex null, true); - PerspexObject.Register(typeof(THost), result); + PerspexPropertyRegistry.Instance.Register(typeof(THost), result); return result; } @@ -468,7 +517,7 @@ namespace Perspex null, true); - PerspexObject.Register(typeof(THost), result); + PerspexPropertyRegistry.Instance.Register(typeof(THost), result); return result; } @@ -529,7 +578,7 @@ namespace Perspex type = type.GetTypeInfo().BaseType; } - return _defaultValues[OwnerType]; + return _defaultValue; } /// diff --git a/src/Perspex.Base/PerspexPropertyRegistry.cs b/src/Perspex.Base/PerspexPropertyRegistry.cs new file mode 100644 index 0000000000..2c388131fa --- /dev/null +++ b/src/Perspex.Base/PerspexPropertyRegistry.cs @@ -0,0 +1,255 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Perspex +{ + /// + /// Tracks registered instances. + /// + public class PerspexPropertyRegistry + { + /// + /// The registered properties by type. + /// + private readonly Dictionary> _registered = + new Dictionary>(); + + /// + /// The registered attached properties by owner type. + /// + private readonly Dictionary> _attached = + new Dictionary>(); + + /// + /// Gets the instance + /// + public static PerspexPropertyRegistry Instance { get; } + = new PerspexPropertyRegistry(); + + /// + /// Gets all attached s registered by an owner. + /// + /// The owner type. + /// A collection of definitions. + public IEnumerable GetAttached(Type ownerType) + { + List list; + + if (_attached.TryGetValue(ownerType, out list)) + { + return list; + } + + return Enumerable.Empty(); + } + + /// + /// Gets all s registered on a type. + /// + /// The type. + /// A collection of definitions. + public IEnumerable GetRegistered(Type type) + { + Contract.Requires(type != null); + + var i = type.GetTypeInfo(); + + while (type != null) + { + List list; + + if (_registered.TryGetValue(type, out list)) + { + foreach (PerspexProperty p in list) + { + yield return p; + } + } + + type = type.GetTypeInfo().BaseType; + } + } + + /// + /// Gets all s registered on a object. + /// + /// The object. + /// A collection of definitions. + public IEnumerable GetRegistered(PerspexObject o) + { + Contract.Requires(o != null); + + return GetRegistered(o.GetType()); + } + + /// + /// Finds registered on a type. + /// + /// The type. + /// The property. + /// The registered property or null if not found. + /// + /// Calling AddOwner on a PerspexProperty creates a new PerspexProperty that is a + /// different object but is equal according to . + /// + public PerspexProperty FindRegistered(Type type, PerspexProperty property) + { + while (type != null) + { + List list; + + if (_registered.TryGetValue(type, out list)) + { + var index = list.IndexOf(property); + + if (index != -1) + { + return list[index]; + } + } + + type = type.GetTypeInfo().BaseType; + } + + return null; + } + + /// + /// Finds registered on an object. + /// + /// The object. + /// The property. + /// The registered property or null if not found. + /// + /// Calling AddOwner on a PerspexProperty creates a new PerspexProperty that is a + /// different object but is equal according to . + /// + public PerspexProperty FindRegistered(object o, PerspexProperty property) + { + return FindRegistered(o.GetType(), property); + } + + /// + /// Finds a registered property on a type by name. + /// + /// The type. + /// + /// The property name. If an attached property it should be in the form + /// "OwnerType.PropertyName". + /// + /// + /// The registered property or null if no matching property found. + /// + public PerspexProperty FindRegistered(Type type, string name) + { + Contract.Requires(type != null); + Contract.Requires(name != null); + + var parts = name.Split('.'); + + if (parts.Length < 1 || parts.Length > 2) + { + throw new ArgumentException("Invalid property name."); + } + + string propertyName; + var results = GetRegistered(type); + + if (parts.Length == 1) + { + propertyName = parts[0]; + } + else + { + var types = GetImplementedTypes(type); + + if (!types.Contains(parts[0])) + { + results = results.Where(x => x.OwnerType.Name == parts[0]); + } + + propertyName = parts[1]; + } + + return results.FirstOrDefault(x => x.Name == propertyName); + } + + private IEnumerable GetImplementedTypes(Type type) + { + while (type != null) + { + yield return type.Name; + type = type.GetTypeInfo().BaseType; + } + } + + /// + /// Checks whether a is registered on a type. + /// + /// The type. + /// The property. + /// True if the property is registered, otherwise false. + public bool IsRegistered(Type type, PerspexProperty property) + { + return FindRegistered(type, property) != null; + } + + /// + /// Checks whether a is registered on a object. + /// + /// The object. + /// The property. + /// True if the property is registered, otherwise false. + public bool IsRegistered(object o, PerspexProperty property) + { + return IsRegistered(o.GetType(), property); + } + + /// + /// Registers a on a type. + /// + /// The type. + /// The property. + /// + /// You won't usually want to call this method directly, instead use the + /// method. + /// + public void Register(Type type, PerspexProperty property) + { + Contract.Requires(type != null); + Contract.Requires(property != null); + + List list; + + if (!_registered.TryGetValue(type, out list)) + { + list = new List(); + _registered.Add(type, list); + } + + if (!list.Contains(property)) + { + list.Add(property); + } + + if (property.IsAttached) + { + if (!_attached.TryGetValue(property.OwnerType, out list)) + { + list = new List(); + _attached.Add(property.OwnerType, list); + } + + if (!list.Contains(property)) + { + list.Add(property); + } + } + } + } +} diff --git a/src/Perspex.Base/PerspexProperty`1.cs b/src/Perspex.Base/PerspexProperty`1.cs index 73ac84e102..6d97833f0a 100644 --- a/src/Perspex.Base/PerspexProperty`1.cs +++ b/src/Perspex.Base/PerspexProperty`1.cs @@ -66,17 +66,29 @@ namespace Perspex Setter = setter; } + /// + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + private PerspexProperty(PerspexProperty source, Type ownerType) + : base(source, ownerType) + { + } + /// /// Initializes a new instance of the class. /// /// The direct property to copy. + /// The new owner type. /// A new getter. /// A new setter. private PerspexProperty( PerspexProperty source, + Type ownerType, Func getter, Action setter) - : base(source, CastParamReturn(getter), CastParams(setter)) + : base(source, ownerType, CastParamReturn(getter), CastParams(setter)) { Getter = getter; Setter = setter; @@ -105,8 +117,9 @@ namespace Perspex "You must provide a new getter and setter when calling AddOwner on a direct PerspexProperty."); } - PerspexObject.Register(typeof(TOwner), this); - return this; + var result = new PerspexProperty(this, typeof(TOwner)); + PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); + return result; } /// @@ -119,8 +132,19 @@ namespace Perspex Action setter = null) where TOwner : PerspexObject { - var result = new PerspexProperty(this, CastReturn(getter), CastParam1(setter)); - PerspexObject.Register(typeof(TOwner), result); + if (!IsDirect) + { + throw new InvalidOperationException( + "This overload of AddOwner is for direct PerspexProperties."); + } + + var result = new PerspexProperty( + this, + typeof(TOwner), + CastReturn(getter), + CastParam1(setter)); + + PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index e41be6ccc3..62150c76b9 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -406,7 +406,11 @@ namespace Perspex.Controls base.OnAttachedToVisualTree(root); IStyler styler = PerspexLocator.Current.GetService(); - styler.ApplyStyles(this); + + if (styler != null) + { + styler.ApplyStyles(this); + } } /// diff --git a/src/Perspex.Controls/DropDown.cs b/src/Perspex.Controls/DropDown.cs index eff7d2c4fe..23aba16009 100644 --- a/src/Perspex.Controls/DropDown.cs +++ b/src/Perspex.Controls/DropDown.cs @@ -80,7 +80,7 @@ namespace Perspex.Controls protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this); + return new ItemContainerGenerator(this, ListBoxItem.ContentProperty); } protected override void OnKeyDown(KeyEventArgs e) diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index 2009131bc5..7189d7a0c9 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -32,26 +32,37 @@ namespace Perspex.Controls.Generators /// The items. /// An optional member selector. /// The created controls. - IList CreateContainers( + IEnumerable Materialize( int startingIndex, IEnumerable items, IMemberSelector selector); /// - /// Removes a set of created containers from the index and returns the removed controls. + /// Removes a set of created containers. /// /// /// The index of the first item of the data in the containing collection. /// - /// The items. - /// The removed controls. - IList RemoveContainers(int startingIndex, IEnumerable items); + /// The the number of items to remove. + /// The removed containers. + IEnumerable Dematerialize(int startingIndex, int count); + + /// + /// Removes a set of created containers and updates the index of later containers to fill + /// the gap. + /// + /// + /// The index of the first item of the data in the containing collection. + /// + /// The the number of items to remove. + /// The removed containers. + IEnumerable RemoveRange(int startingIndex, int count); /// - /// Clears the created containers from the index and returns the removed controls. + /// Clears all created containers and returns the removed controls. /// /// The removed controls. - IList ClearContainers(); + IEnumerable Clear(); /// /// Gets the container control representing the item with the specified index. diff --git a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs index 0555fd436f..37f097618b 100644 --- a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs @@ -11,23 +11,23 @@ namespace Perspex.Controls.Generators public interface ITreeItemContainerGenerator : IItemContainerGenerator { /// - /// Gets all of the generated container controls. + /// Gets the item container for the root of the tree, or null if this generator is itself + /// the root of the tree. /// - /// The containers. - IEnumerable GetAllContainers(); + ITreeItemContainerGenerator RootGenerator { get; } /// - /// Gets the item that is contained by the specified container. + /// Gets the item container for the specified item, anywhere in the tree. /// - /// The container. - /// The item. - object ItemFromContainer(IControl container); + /// The item. + /// The container, or null if not found. + IControl TreeContainerFromItem(object item); /// - /// Gets the container for the specified item + /// Gets the item for the specified item container, anywhere in the tree. /// - /// The item. - /// The container. - IControl ContainerFromItem(object item); + /// The container. + /// The item, or null if not found. + object TreeItemFromContainer(IControl container); } } \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index 9e8183c322..b8b57ddd59 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -15,7 +15,7 @@ namespace Perspex.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private Dictionary _containers = new Dictionary(); + private List _containers = new List(); private readonly Subject _containersInitialized = new Subject(); @@ -25,17 +25,15 @@ namespace Perspex.Controls.Generators /// The owner control. public ItemContainerGenerator(IControl owner) { + Contract.Requires(owner != null); + Owner = owner; } - /// - /// Gets the currently realized containers. - /// - public IEnumerable Containers => _containers.Values; + /// + public IEnumerable Containers => _containers; - /// - /// Signalled whenever new containers are initialized. - /// + /// public IObservable ContainersInitialized => _containersInitialized; /// @@ -43,16 +41,8 @@ namespace Perspex.Controls.Generators /// public IControl Owner { get; } - /// - /// Creates container controls for a collection of items. - /// - /// - /// The index of the first item of the data in the containing collection. - /// - /// The items. - /// An optional member selector. - /// The created container controls. - public IList CreateContainers( + /// + public IEnumerable Materialize( int startingIndex, IEnumerable items, IMemberSelector selector) @@ -75,72 +65,54 @@ namespace Perspex.Controls.Generators return result.Where(x => x != null).ToList(); } - /// - /// Removes a set of created containers from the index and returns the removed controls. - /// - /// - /// The index of the first item of the data in the containing collection. - /// - /// The items. - /// The removed controls. - public IList RemoveContainers(int startingIndex, IEnumerable items) + /// + public virtual IEnumerable Dematerialize(int startingIndex, int count) { var result = new List(); - var count = items.Cast().Count(); for (int i = startingIndex; i < startingIndex + count; ++i) { - var container = _containers[i]; - - if (container != null) + if (i < _containers.Count) { - result.Add(container); - _containers.Remove(i); + result.Add(_containers[i]); + _containers[i] = null; } } return result; } - /// - /// Clears the created containers from the index and returns the removed controls. - /// - /// The removed controls. - public IList ClearContainers() + /// + public virtual IEnumerable RemoveRange(int startingIndex, int count) { - var result = _containers; - _containers = new Dictionary(); - return result.Values.ToList(); + var result = _containers.GetRange(startingIndex, count); + _containers.RemoveRange(startingIndex, count); + return result; } - /// - /// Gets the container control representing the item with the specified index. - /// - /// The index. - /// The container or null if no container created. - public IControl ContainerFromIndex(int index) + /// + public virtual IEnumerable Clear() { - IControl result; - _containers.TryGetValue(index, out result); + var result = _containers; + _containers = new List(); return result; } - /// - /// Gets the index of the specified container control. - /// - /// The container. - /// The index of the container or -1 if not found. - public int IndexFromContainer(IControl container) + /// + public IControl ContainerFromIndex(int index) { - foreach (var i in _containers) + if (index < _containers.Count) { - if (i.Value == container) - { - return i.Key; - } + return _containers[index]; } - return -1; + return null; + } + + /// + public int IndexFromContainer(IControl container) + { + return _containers.IndexOf(container); } /// @@ -171,7 +143,16 @@ namespace Perspex.Controls.Generators foreach (var c in container) { - if (!_containers.ContainsKey(index)) + while (_containers.Count < index) + { + _containers.Add(null); + } + + if (_containers.Count == index) + { + _containers.Add(c); + } + else if (_containers[index] == null) { _containers[index] = c; } @@ -183,5 +164,16 @@ namespace Perspex.Controls.Generators ++index; } } + + /// + /// Gets all containers with an index that fall within a range. + /// + /// The first index. + /// The number of elements in the range. + /// The containers. + protected IEnumerable GetContainerRange(int index, int count) + { + return _containers.GetRange(index, count); + } } } \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs index fca5d1b4f8..dcb365b120 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs @@ -1,6 +1,9 @@ // 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.Expressions; +using System.Reflection; using Perspex.Controls.Templates; namespace Perspex.Controls.Generators @@ -9,17 +12,26 @@ namespace Perspex.Controls.Generators /// Creates containers for items and maintains a list of created containers. /// /// The type of the container. - public class ItemContainerGenerator : ItemContainerGenerator where T : class, IContentControl, new() + public class ItemContainerGenerator : ItemContainerGenerator where T : class, IControl, new() { /// /// Initializes a new instance of the class. /// /// The owner control. - public ItemContainerGenerator(Control owner) + /// The container's Content property. + public ItemContainerGenerator( + IControl owner, + PerspexProperty contentProperty) : base(owner) { + ContentProperty = contentProperty; } + /// + /// Gets the container's Content property. + /// + protected PerspexProperty ContentProperty { get; } + /// protected override IControl CreateContainer(object item) { @@ -36,7 +48,7 @@ namespace Perspex.Controls.Generators else { var result = new T(); - result.Content = Owner.MaterializeDataTemplate(item); + result.SetValue(ContentProperty, Owner.MaterializeDataTemplate(item)); if (!(item is IControl)) { diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index fa8c699ec3..18df4c4183 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -1,11 +1,8 @@ // 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.Collections; using System.Collections.Generic; -using System.Linq; -using System.Reactive.Subjects; using Perspex.Controls.Templates; namespace Perspex.Controls.Generators @@ -14,161 +11,84 @@ namespace Perspex.Controls.Generators /// Creates containers for tree items and maintains a list of created containers. /// /// The type of the container. - public class TreeItemContainerGenerator : ITreeItemContainerGenerator where T : TreeViewItem, new() + public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator + where T : class, IControl, new() { - private Dictionary _containers = new Dictionary(); - - private readonly Subject _containersInitialized = new Subject(); + private Dictionary _itemToContainer; + private Dictionary _containerToItem; /// /// Initializes a new instance of the class. /// /// The owner control. - public TreeItemContainerGenerator(IControl owner) - { - Owner = owner; - } - - /// - /// Gets the currently realized containers. - /// - public IEnumerable Containers => _containers.Values; - - /// - /// Signalled whenever new containers are initialized. - /// - public IObservable ContainersInitialized => _containersInitialized; - - /// - /// Gets the owner control. - /// - public IControl Owner { get; } - - /// - /// Creates container controls for a collection of items. - /// - /// - /// The index of the first item of the data in the containing collection. - /// - /// The items. - /// An optional member selector. - /// The created container controls. - public IList CreateContainers( - int startingIndex, - IEnumerable items, - IMemberSelector selector) - { - Contract.Requires(items != null); - - int index = startingIndex; - var result = new List(); - - foreach (var item in items) - { - var i = selector != null ? selector.Select(item) : item; - var container = CreateContainer(i); - _containers.Add(i, container); - result.Add(container); - } - - _containersInitialized.OnNext(new ItemContainers(startingIndex, result)); - - return result.Where(x => x != null).ToList(); - } - - /// - /// Removes a set of created containers from the index and returns the removed controls. - /// - /// - /// The index of the first item of the data in the containing collection. + /// The container's Content property. + /// The container's Items property. + /// The container's IsExpanded property. + /// + /// The item container for the root of the tree, or null if this generator is itself the + /// root of the tree. /// - /// The items. - /// The removed controls. - public IList RemoveContainers(int startingIndex, IEnumerable items) + public TreeItemContainerGenerator( + IControl owner, + PerspexProperty contentProperty, + PerspexProperty itemsProperty, + PerspexProperty isExpandedProperty, + ITreeItemContainerGenerator rootGenerator) + : base(owner, contentProperty) { - var result = new List(); + ItemsProperty = itemsProperty; + IsExpandedProperty = isExpandedProperty; + RootGenerator = rootGenerator; - foreach (var item in items) + if (rootGenerator == null) { - T container; - - if (_containers.TryGetValue(item, out container)) - { - Remove(container, result); - } + _itemToContainer = new Dictionary(); + _containerToItem = new Dictionary(); } - - return result; } /// - /// Clears the created containers from the index and returns the removed controls. + /// Gets the item container for the root of the tree, or null if this generator is itself + /// the root of the tree. /// - /// The removed controls. - public IList ClearContainers() - { - var result = _containers; - _containers = new Dictionary(); - return result.Values.Cast().ToList(); - } + public ITreeItemContainerGenerator RootGenerator { get; } /// - /// Gets the container control representing the item with the specified index. + /// Gets the item container's Items property. /// - /// The index. - /// The container or null if no container created. - public IControl ContainerFromIndex(int index) - { - throw new NotImplementedException(); - } + protected PerspexProperty ItemsProperty { get; } /// - /// Gets the index of the specified container control. + /// Gets the item container's IsExpanded property. /// - /// The container. - /// The index of the container or -1 if not found. - public int IndexFromContainer(IControl container) - { - throw new NotImplementedException(); - } + protected PerspexProperty IsExpandedProperty { get; } /// - /// Gets all of the generated container controls. + /// Gets the item container for the specified item, anywhere in the tree. /// - /// The containers. - public IEnumerable GetAllContainers() + /// The item. + /// The container, or null if not found. + public IControl TreeContainerFromItem(object item) { - return _containers.Values; + T result; + _itemToContainer.TryGetValue(item, out result); + return result; } /// - /// Gets the item that is contained by the specified container. + /// Gets the item for the specified item container, anywhere in the tree. /// /// The container. - /// The item. - public object ItemFromContainer(IControl container) - { - return container.DataContext; - } - - /// - /// Gets the container for the specified item - /// - /// The item. - /// The container. - public IControl ContainerFromItem(object item) + /// The item, or null if not found. + public object TreeItemFromContainer(IControl container) { - T result; - _containers.TryGetValue(item, out result); + object result; + _containerToItem.TryGetValue(container, out result); return result; } - /// - /// Creates the container for an item. - /// - /// The item. - /// The created container control. - protected virtual T CreateContainer(object item) + /// + protected override IControl CreateContainer(object item) { var container = item as T; @@ -183,22 +103,78 @@ namespace Perspex.Controls.Generators else { var template = GetTreeDataTemplate(item); - var result = new T - { - Header = template.Build(item), - Items = template.ItemsSelector(item), - IsExpanded = template.IsExpanded(item), - }; + var result = new T(); + + result.SetValue(ContentProperty, template.Build(item)); + result.SetValue(ItemsProperty, template.ItemsSelector(item)); + result.SetValue(IsExpandedProperty, template.IsExpanded(item)); if (!(item is IControl)) { result.DataContext = item; } + AddToIndex(item, result); + return result; } } + public override IEnumerable Clear() + { + ClearIndex(); + return base.Clear(); + } + + public override IEnumerable Dematerialize(int startingIndex, int count) + { + RemoveFromIndex(GetContainerRange(startingIndex, count)); + return base.Dematerialize(startingIndex, count); + } + + private void AddToIndex(object item, T container) + { + if (RootGenerator != null) + { + ((TreeItemContainerGenerator)RootGenerator).AddToIndex(item, container); + } + else + { + _itemToContainer.Add(item, container); + _containerToItem.Add(container, item); + } + } + + private void RemoveFromIndex(IEnumerable containers) + { + if (RootGenerator != null) + { + ((TreeItemContainerGenerator)RootGenerator).RemoveFromIndex(containers); + } + else + { + foreach (var container in containers) + { + var item = _containerToItem[container]; + _containerToItem.Remove(container); + _itemToContainer.Remove(item); + } + } + } + + private void ClearIndex() + { + if (RootGenerator != null) + { + ((TreeItemContainerGenerator)RootGenerator).ClearIndex(); + } + else + { + _containerToItem.Clear(); + _itemToContainer.Clear(); + } + } + /// /// Gets the data template for the specified item. /// @@ -222,29 +198,5 @@ namespace Perspex.Controls.Generators return treeTemplate; } - - private void Remove(T container, IList removed) - { - if (container.Items != null) - { - foreach (var childItem in container.Items) - { - T childContainer; - - if (_containers.TryGetValue(childItem, out childContainer)) - { - Remove(childContainer, removed); - } - } - } - - // TODO: Dual index. - var i = _containers.FirstOrDefault(x => x.Value == container); - - if (i.Key != null) - { - _containers.Remove(i.Key); - } - } } } diff --git a/src/Perspex.Controls/ListBox.cs b/src/Perspex.Controls/ListBox.cs index 1ecd086cae..7641e2e54a 100644 --- a/src/Perspex.Controls/ListBox.cs +++ b/src/Perspex.Controls/ListBox.cs @@ -7,6 +7,7 @@ using Perspex.Collections; using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Input; +using Perspex.Interactivity; namespace Perspex.Controls { @@ -43,7 +44,7 @@ namespace Perspex.Controls /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this); + return new ItemContainerGenerator(this, ListBoxItem.ContentProperty); } /// diff --git a/src/Perspex.Controls/Presenters/CarouselPresenter.cs b/src/Perspex.Controls/Presenters/CarouselPresenter.cs index 4687740adb..7af2416b9a 100644 --- a/src/Perspex.Controls/Presenters/CarouselPresenter.cs +++ b/src/Perspex.Controls/Presenters/CarouselPresenter.cs @@ -199,7 +199,7 @@ namespace Perspex.Controls.Presenters if (toIndex != -1) { var item = Items.Cast().ElementAt(toIndex); - to = generator.CreateContainers(toIndex, new[] { item }, MemberSelector).FirstOrDefault(); + to = generator.Materialize(toIndex, new[] { item }, MemberSelector).FirstOrDefault(); if (to != null) { @@ -215,7 +215,7 @@ namespace Perspex.Controls.Presenters if (from != null) { Panel.Children.Remove(from); - generator.RemoveContainers(fromIndex, new[] { from }); + generator.Dematerialize(fromIndex, 1); } } diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 5c52f70df4..ecd74e7bdb 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; using Perspex.Controls.Generators; using Perspex.Controls.Templates; @@ -186,7 +187,7 @@ namespace Perspex.Controls.Presenters { if (items != null) { - Panel.Children.AddRange(ItemContainerGenerator.CreateContainers(0, Items, MemberSelector)); + Panel.Children.AddRange(ItemContainerGenerator.Materialize(0, Items, MemberSelector)); INotifyCollectionChanged incc = items as INotifyCollectionChanged; @@ -209,7 +210,7 @@ namespace Perspex.Controls.Presenters if (e.OldValue != null) { - generator.ClearContainers(); + generator.Clear(); Panel.Children.Clear(); INotifyCollectionChanged incc = e.OldValue as INotifyCollectionChanged; @@ -237,18 +238,39 @@ namespace Perspex.Controls.Presenters if (_createdPanel) { var generator = ItemContainerGenerator; + IEnumerable containers; // TODO: Handle Move and Replace etc. switch (e.Action) { case NotifyCollectionChangedAction.Add: - Panel.Children.AddRange( - generator.CreateContainers(e.NewStartingIndex, e.NewItems, MemberSelector)); + containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); + Panel.Children.AddRange(containers); break; case NotifyCollectionChangedAction.Remove: - Panel.Children.RemoveAll( - generator.RemoveContainers(e.OldStartingIndex, e.OldItems)); + containers = generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count); + Panel.Children.RemoveAll(containers); + break; + + case NotifyCollectionChangedAction.Replace: + generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count); + containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); + + var i = e.NewStartingIndex; + + foreach (var container in containers) + { + Panel.Children[i++] = container; + } + + break; + + case NotifyCollectionChangedAction.Move: + // TODO: Implement Move in a more efficient manner. + case NotifyCollectionChangedAction.Reset: + Panel.Children.RemoveAll(generator.Clear()); + Panel.Children.AddRange(generator.Materialize(0, Items, MemberSelector)); break; } diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index f1de2dea5f..f9c430d393 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -141,7 +141,7 @@ namespace Perspex.Controls.Primitives var index = IndexOf(Items, value); var effective = index != -1 ? value : null; - if (effective != old) + if (!object.Equals(effective, old)) { _selectedItem = effective; RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue); @@ -388,8 +388,8 @@ namespace Perspex.Controls.Primitives } /// - /// Updates the selection based on an event source that may have originated in a container - /// that belongs to the control. + /// Updates the selection based on an event that may have originated in a container that + /// belongs to the control. /// /// The control that raised the event. /// Whether the container should be selected or unselected. @@ -405,11 +405,11 @@ namespace Perspex.Controls.Primitives bool rangeModifier = false, bool toggleModifier = false) { - var item = GetContainerFromEventSource(eventSource); + var container = GetContainerFromEventSource(eventSource); - if (item != null) + if (container != null) { - UpdateSelection(item, select, rangeModifier, toggleModifier); + UpdateSelection(container, select, rangeModifier, toggleModifier); return true; } diff --git a/src/Perspex.Controls/Primitives/TabStrip.cs b/src/Perspex.Controls/Primitives/TabStrip.cs index 54b0abbf4c..5fc305c73a 100644 --- a/src/Perspex.Controls/Primitives/TabStrip.cs +++ b/src/Perspex.Controls/Primitives/TabStrip.cs @@ -43,7 +43,7 @@ namespace Perspex.Controls.Primitives } else { - result = new ItemContainerGenerator(this); + result = new ItemContainerGenerator(this, TabItem.ContentProperty); } return result; diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index c077d42aee..a1d14d5441 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/src/Perspex.Controls/TreeView.cs @@ -6,11 +6,20 @@ using System.Linq; using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Input; +using Perspex.Interactivity; +using Perspex.Styling; +using Perspex.VisualTree; namespace Perspex.Controls { + /// + /// Displays a hierachical tree of data. + /// public class TreeView : ItemsControl { + /// + /// Defines the property. + /// public static readonly PerspexProperty SelectedItemProperty = SelectingItemsControl.SelectedItemProperty.AddOwner( o => o.SelectedItem, @@ -18,61 +27,174 @@ namespace Perspex.Controls private object _selectedItem; + /// + /// Initializes static members of the class. + /// static TreeView() { - SelectedItemProperty.Changed.Subscribe(x => - { - var control = x.Sender as TreeView; - - if (control != null) - { - control.SelectedItemChanged(x.NewValue); - } - }); + // HACK: Needed or SelectedItem property will not be found in Release build. } - public new ITreeItemContainerGenerator ItemContainerGenerator => (ITreeItemContainerGenerator)base.ItemContainerGenerator; + /// + /// Gets the for the tree view. + /// + public new ITreeItemContainerGenerator ItemContainerGenerator => + (ITreeItemContainerGenerator)base.ItemContainerGenerator; + /// + /// Gets or sets the selected item. + /// public object SelectedItem { get { return _selectedItem; } set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); } } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new TreeItemContainerGenerator(this); + return new TreeItemContainerGenerator( + this, + TreeViewItem.HeaderProperty, + TreeViewItem.ItemsProperty, + TreeViewItem.IsExpandedProperty, + null); } + /// protected override void OnGotFocus(GotFocusEventArgs e) { - var control = (IControl)e.Source; - var item = ItemContainerGenerator.ItemFromContainer(control); + if (e.NavigationMethod == NavigationMethod.Directional) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.InputModifiers & InputModifiers.Shift) != 0); + } + } + + /// + protected override void OnPointerPressed(PointerPressEventArgs e) + { + base.OnPointerPressed(e); + + if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.InputModifiers & InputModifiers.Shift) != 0, + (e.InputModifiers & InputModifiers.Control) != 0); + } + } + + /// + /// Updates the selection for an item based on user interaction. + /// + /// The container. + /// Whether the item should be selected or unselected. + /// Whether the range modifier is enabled (i.e. shift key). + /// Whether the toggle modifier is enabled (i.e. ctrl key). + protected void UpdateSelectionFromContainer( + IControl container, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) + { + var item = ItemContainerGenerator.TreeItemFromContainer(container); if (item != null) { + if (SelectedItem != null) + { + var old = ItemContainerGenerator.TreeContainerFromItem(SelectedItem); + MarkContainerSelected(old, false); + } + SelectedItem = item; - e.Handled = true; + + if (SelectedItem != null) + { + MarkContainerSelected(container, true); + } } } - private void SelectedItemChanged(object selected) + /// + /// Updates the selection based on an event that may have originated in a container that + /// belongs to the control. + /// + /// The control that raised the event. + /// Whether the container should be selected or unselected. + /// Whether the range modifier is enabled (i.e. shift key). + /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// + /// True if the event originated from a container that belongs to the control; otherwise + /// false. + /// + protected bool UpdateSelectionFromEventSource( + IInteractive eventSource, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) { - var containers = ItemContainerGenerator.GetAllContainers().OfType(); - var selectedContainer = (selected != null) ? - ItemContainerGenerator.ContainerFromItem(selected) : - null; + var container = GetContainerFromEventSource(eventSource); - if (Presenter != null && Presenter.Panel != null) + if (container != null) { - KeyboardNavigation.SetTabOnceActiveElement( - (InputElement)Presenter.Panel, - selectedContainer); + UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier); + return true; } - foreach (var item in containers) + return false; + } + + /// + /// Tries to get the container that was the source of an event. + /// + /// The control that raised the event. + /// The container or null if the event did not originate in a container. + protected IControl GetContainerFromEventSource(IInteractive eventSource) + { + var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(); + + if (item != null) + { + if (item.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator) + { + return item; + } + } + + return null; + } + + /// + /// Sets a container's 'selected' class or . + /// + /// The container. + /// Whether the control is selected + private void MarkContainerSelected(IControl container, bool selected) + { + var selectable = container as ISelectable; + var styleable = container as IStyleable; + + if (selectable != null) + { + selectable.IsSelected = selected; + } + else if (styleable != null) { - item.IsSelected = item == selectedContainer; + if (selected) + { + styleable.Classes.Add(":selected"); + } + else + { + styleable.Classes.Remove(":selected"); + } } } } diff --git a/src/Perspex.Controls/TreeViewItem.cs b/src/Perspex.Controls/TreeViewItem.cs index 19bbdb1e7b..bd499e4f10 100644 --- a/src/Perspex.Controls/TreeViewItem.cs +++ b/src/Perspex.Controls/TreeViewItem.cs @@ -1,10 +1,9 @@ // 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 Perspex.Controls.Mixins; using Perspex.Controls.Generators; +using Perspex.Controls.Mixins; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Input; @@ -66,39 +65,31 @@ namespace Perspex.Controls set { SetValue(IsSelectedProperty, value); } } + /// + /// Gets the for the tree view. + /// + public new ITreeItemContainerGenerator ItemContainerGenerator => + (ITreeItemContainerGenerator)base.ItemContainerGenerator; + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - if (_treeView == null) - { - throw new InvalidOperationException( - "Cannot get the ItemContainerGenerator for a TreeViewItem " + - "before it is added to a TreeView."); - } - - return _treeView.ItemContainerGenerator; + return new TreeItemContainerGenerator( + this, + TreeViewItem.HeaderProperty, + TreeViewItem.ItemsProperty, + TreeViewItem.IsExpandedProperty, + _treeView?.ItemContainerGenerator); } /// protected override void OnAttachedToVisualTree(IRenderRoot root) { base.OnAttachedToVisualTree(root); - - if (this.GetVisualParent() != null) - { - _treeView = this.GetVisualAncestors().OfType().FirstOrDefault(); - - if (_treeView == null) - { - throw new InvalidOperationException("TreeViewItems must be added to a TreeView."); - } - } - else - { - _treeView = null; - } + _treeView = this.GetVisualAncestors().OfType().FirstOrDefault(); } + /// protected override void OnKeyDown(KeyEventArgs e) { if (!e.Handled) diff --git a/src/Perspex.Diagnostics/Debug.cs b/src/Perspex.Diagnostics/Debug.cs index d5362c3257..1e07363a71 100644 --- a/src/Perspex.Diagnostics/Debug.cs +++ b/src/Perspex.Diagnostics/Debug.cs @@ -37,7 +37,7 @@ namespace Perspex.Diagnostics builder.Append(" "); builder.AppendLine(control.Classes.ToString()); - foreach (var property in control.GetRegisteredProperties()) + foreach (var property in PerspexPropertyRegistry.Instance.GetRegistered(control)) { var value = control.GetDiagnostic(property); diff --git a/src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs index 8a45ec8244..73abcf4a04 100644 --- a/src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -14,7 +14,7 @@ namespace Perspex.Diagnostics.ViewModels { if (control != null) { - Properties = control.GetRegisteredProperties() + Properties = PerspexPropertyRegistry.Instance.GetRegistered(control) .Select(x => new PropertyDetails(control, x)) .OrderBy(x => x.IsAttached) .ThenBy(x => x.Name); diff --git a/src/Perspex.Layout/Layoutable.cs b/src/Perspex.Layout/Layoutable.cs index 09c4e1c103..ead1ef88bc 100644 --- a/src/Perspex.Layout/Layoutable.cs +++ b/src/Perspex.Layout/Layoutable.cs @@ -505,6 +505,12 @@ namespace Perspex.Layout height = Math.Max(height, child.DesiredSize.Height); } + if (UseLayoutRounding) + { + width = Math.Ceiling(width); + height = Math.Ceiling(height); + } + return new Size(width, height); } diff --git a/src/Perspex.Themes.Default/TreeViewItemStyle.cs b/src/Perspex.Themes.Default/TreeViewItemStyle.cs index b485cea22e..7b3352ad0f 100644 --- a/src/Perspex.Themes.Default/TreeViewItemStyle.cs +++ b/src/Perspex.Themes.Default/TreeViewItemStyle.cs @@ -13,6 +13,7 @@ using Perspex.Styling; namespace Perspex.Themes.Default { + using Collections; using Controls = Controls.Controls; /// @@ -116,6 +117,15 @@ namespace Perspex.Themes.Default { [~ContentPresenter.ContentProperty] = control[~HeaderedItemsControl.HeaderProperty], }, + }, + new Rectangle + { + Name = "focus", + Stroke = Brushes.Black, + StrokeThickness = 1, + StrokeDashArray = new PerspexList(1, 2), + [Grid.ColumnProperty] = 1, + [!Rectangle.IsVisibleProperty] = control[!TreeViewItem.IsFocusedProperty], } } }, diff --git a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index e343d3e512..398d073db5 100644 --- a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj +++ b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj @@ -75,6 +75,7 @@ + diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs index 7fa816cf82..4233806b65 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs @@ -18,30 +18,6 @@ namespace Perspex.Base.UnitTests p = AttachedOwner.AttachedProperty; } - [Fact] - public void GetRegisteredProperties_Returns_Registered_Properties() - { - string[] names = PerspexObject.GetRegisteredProperties(typeof(Class1)).Select(x => x.Name).ToArray(); - - Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names); - } - - [Fact] - public void GetRegisteredProperties_Returns_Registered_Properties_For_Base_Types() - { - string[] names = PerspexObject.GetRegisteredProperties(typeof(Class2)).Select(x => x.Name).ToArray(); - - Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names); - } - - [Fact] - public void GetAttachedProperties_Returns_Registered_Properties_For_Base_Types() - { - string[] names = PerspexObject.GetAttachedProperties(typeof(AttachedOwner)).Select(x => x.Name).ToArray(); - - Assert.Equal(new[] { "Attached" }, names); - } - [Fact] public void IsSet_Returns_False_For_Unset_Property() { diff --git a/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs b/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs new file mode 100644 index 0000000000..e2e60c04dc --- /dev/null +++ b/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs @@ -0,0 +1,177 @@ +// 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.Linq; +using System.Reactive.Linq; +using Xunit; + +namespace Perspex.Base.UnitTests +{ + public class PerspexPropertyRegistryTests + { + public PerspexPropertyRegistryTests() + { + // Ensure properties are registered. + PerspexProperty p; + p = Class1.FooProperty; + p = Class2.BarProperty; + p = AttachedOwner.AttachedProperty; + } + + [Fact] + public void GetRegistered_Returns_Registered_Properties() + { + string[] names = PerspexPropertyRegistry.Instance.GetRegistered(typeof(Class1)) + .Select(x => x.Name) + .ToArray(); + + Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names); + } + + [Fact] + public void GetRegistered_Returns_Registered_Properties_For_Base_Types() + { + string[] names = PerspexPropertyRegistry.Instance.GetRegistered(typeof(Class2)) + .Select(x => x.Name) + .ToArray(); + + Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names); + } + + [Fact] + public void GetAttached_Returns_Registered_Properties_For_Base_Types() + { + string[] names = PerspexPropertyRegistry.Instance.GetAttached(typeof(AttachedOwner)).Select(x => x.Name).ToArray(); + + Assert.Equal(new[] { "Attached" }, names); + } + + [Fact] + public void FindRegistered_Finds_Untyped_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Foo"); + + Assert.Equal(Class1.FooProperty, result); + } + + [Fact] + public void FindRegistered_Finds_Typed_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Class1.Foo"); + + Assert.Equal(Class1.FooProperty, result); + } + + [Fact] + public void FindRegistered_Finds_Typed_Inherited_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Class1.Foo"); + + Assert.Equal(Class2.FooProperty, result); + } + + [Fact] + public void FindRegistered_Finds_Inherited_Property_With_Derived_Type_Name() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Class2.Foo"); + + Assert.Equal(Class2.FooProperty, result); + } + + [Fact] + public void FindRegistered_Finds_Attached_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class2), "AttachedOwner.Attached"); + + Assert.Equal(AttachedOwner.AttachedProperty, result); + } + + [Fact] + public void FindRegistered_Finds_AddOwnered_Untyped_Attached_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Attached"); + + Assert.Equal(AttachedOwner.AttachedProperty, result); + } + + [Fact] + public void FindRegistered_Finds_AddOwnered_Typed_Attached_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Class3.Attached"); + + Assert.Equal(AttachedOwner.AttachedProperty, result); + } + + [Fact] + public void FindRegistered_Finds_AddOwnered_AttachedTyped_Attached_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class3), "AttachedOwner.Attached"); + + Assert.Equal(AttachedOwner.AttachedProperty, result); + } + + [Fact] + public void FindRegistered_Finds_AddOwnered_BaseTyped_Attached_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Class1.Attached"); + + Assert.Equal(AttachedOwner.AttachedProperty, result); + } + + [Fact] + public void FindRegistered_Doesnt_Find_Nonregistered_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Bar"); + + Assert.Null(result); + } + + [Fact] + public void FindRegistered_Doesnt_Find_Nonregistered_Attached_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class4), "AttachedOwner.Attached"); + + Assert.Null(result); + } + + private class Class1 : PerspexObject + { + public static readonly PerspexProperty FooProperty = + PerspexProperty.Register("Foo"); + + public static readonly PerspexProperty BazProperty = + PerspexProperty.Register("Baz"); + + public static readonly PerspexProperty QuxProperty = + PerspexProperty.Register("Qux"); + } + + private class Class2 : Class1 + { + public static readonly PerspexProperty BarProperty = + PerspexProperty.Register("Bar"); + + public static readonly PerspexProperty FlobProperty = + PerspexProperty.Register("Flob"); + + public static readonly PerspexProperty FredProperty = + PerspexProperty.Register("Fred"); + } + + private class Class3 : Class1 + { + public static readonly PerspexProperty AttachedProperty = + AttachedOwner.AttachedProperty.AddOwner(); + } + + public class Class4 : PerspexObject + { + } + + private class AttachedOwner + { + public static readonly PerspexProperty AttachedProperty = + PerspexProperty.RegisterAttached("Attached"); + } + } +} diff --git a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs b/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs index 6c90ac1039..c858152da5 100644 --- a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs +++ b/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs @@ -161,6 +161,15 @@ namespace Perspex.Base.UnitTests Assert.True(p1 == p2); } + [Fact] + public void AddOwnered_Property_Should_Have_OwnerType_Set() + { + var p1 = new PerspexProperty("p1", typeof(Class1)); + var p2 = p1.AddOwner(); + + Assert.Equal(typeof(Class3), p2.OwnerType); + } + [Fact] public void AddOwnered_Direct_Property_Should_Equal_Original() { @@ -172,6 +181,15 @@ namespace Perspex.Base.UnitTests Assert.True(p1 == p2); } + [Fact] + public void AddOwnered_Direct_Property_Should_Have_OwnerType_Set() + { + var p1 = new PerspexProperty("d1", typeof(Class1), o => null, (o, v) => { }); + var p2 = p1.AddOwner(o => null, (o, v) => { }); + + Assert.Equal(typeof(Class3), p2.OwnerType); + } + [Fact] public void AddOwner_With_Getter_And_Setter_On_Standard_Property_Should_Throw() { diff --git a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs new file mode 100644 index 0000000000..d8d2c6ba1f --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -0,0 +1,93 @@ +// 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.Linq; +using Perspex.Controls.Generators; +using Xunit; + +namespace Perspex.Controls.UnitTests.Generators +{ + public class ItemContainerGeneratorTests + { + [Fact] + public void Materialize_Should_Create_Containers() + { + var items = new[] { "foo", "bar", "baz" }; + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner); + var containers = target.Materialize(0, items, null); + var result = containers.OfType().Select(x => x.Text).ToList(); + + Assert.Equal(items, result); + } + + [Fact] + public void ContainerFromIndex_Should_Return_Materialized_Containers() + { + var items = new[] { "foo", "bar", "baz" }; + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner); + var containers = target.Materialize(0, items, null).ToList(); + + Assert.Equal(containers[0], target.ContainerFromIndex(0)); + Assert.Equal(containers[1], target.ContainerFromIndex(1)); + Assert.Equal(containers[2], target.ContainerFromIndex(2)); + } + + [Fact] + public void IndexFromContainer_Should_Return_Index() + { + var items = new[] { "foo", "bar", "baz" }; + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner); + var containers = target.Materialize(0, items, null).ToList(); + + Assert.Equal(0, target.IndexFromContainer(containers[0])); + Assert.Equal(1, target.IndexFromContainer(containers[1])); + Assert.Equal(2, target.IndexFromContainer(containers[2])); + } + + [Fact] + public void Dematerialize_Should_Remove_Container() + { + var items = new[] { "foo", "bar", "baz" }; + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner); + var containers = target.Materialize(0, items, null).ToList(); + + target.Dematerialize(1, 1); + + Assert.Equal(containers[0], target.ContainerFromIndex(0)); + Assert.Equal(null, target.ContainerFromIndex(1)); + Assert.Equal(containers[2], target.ContainerFromIndex(2)); + } + + [Fact] + public void Dematerialize_Should_Return_Removed_Containers() + { + var items = new[] { "foo", "bar", "baz" }; + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner); + var containers = target.Materialize(0, items, null); + var expected = target.Containers.Take(2).ToList(); + var result = target.Dematerialize(0, 2); + + Assert.Equal(expected, result); + } + + [Fact] + public void RemoveRange_Should_Alter_Successive_Container_Indexes() + { + var items = new[] { "foo", "bar", "baz" }; + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner); + var containers = target.Materialize(0, items, null).ToList(); + + var removed = target.RemoveRange(1, 1).Single(); + + Assert.Equal(containers[0], target.ContainerFromIndex(0)); + Assert.Equal(containers[2], target.ContainerFromIndex(1)); + Assert.Equal(containers[1], removed); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs new file mode 100644 index 0000000000..f6b0ff2eae --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs @@ -0,0 +1,28 @@ +// 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.Linq; +using Perspex.Controls.Generators; +using Xunit; + +namespace Perspex.Controls.UnitTests.Generators +{ + public class ItemContainerGeneratorTypedTests + { + [Fact] + public void Materialize_Should_Create_Containers() + { + var items = new[] { "foo", "bar", "baz" }; + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty); + var containers = target.Materialize(0, items, null); + var result = containers + .OfType() + .Select(x => x.Content) + .OfType() + .Select(x => x.Text).ToList(); + + Assert.Equal(items, result); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index a66afeedc2..912bcd534a 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -81,6 +81,8 @@ + + diff --git a/tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs index 4ce53c6142..7997e0cb90 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs @@ -74,7 +74,7 @@ namespace Perspex.Controls.UnitTests.Presenters { protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this); + return new ItemContainerGenerator(this, TestItem.ContentProperty); } } } diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 50378047dc..db9e6d3aec 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.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.ObjectModel; using System.Linq; using Perspex.Collections; using Perspex.Controls.Generators; @@ -39,7 +40,9 @@ namespace Perspex.Controls.UnitTests.Presenters Items = new[] { "foo", "bar" }, }; - target.ItemContainerGenerator = new ItemContainerGenerator(target); + target.ItemContainerGenerator = new ItemContainerGenerator( + target, + ListBoxItem.ContentProperty); target.ApplyTemplate(); Assert.Equal(2, target.Panel.Children.Count); @@ -73,10 +76,67 @@ namespace Perspex.Controls.UnitTests.Presenters Assert.Equal(1, target.Panel.Children.Count); Assert.Equal("bar", ((TextBlock)target.Panel.Children[0]).Text); + Assert.Equal("bar", ((TextBlock)target.ItemContainerGenerator.ContainerFromIndex(0)).Text); } [Fact] public void Clearing_Items_Should_Remove_Containers() + { + var items = new ObservableCollection { "foo", "bar" }; + var target = new ItemsPresenter + { + Items = items, + }; + + target.ApplyTemplate(); + items.Clear(); + + Assert.Empty(target.Panel.Children); + Assert.Empty(target.ItemContainerGenerator.Containers); + } + + [Fact] + public void Replacing_Items_Should_Update_Containers() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = new ItemsPresenter + { + Items = items, + }; + + target.ApplyTemplate(); + items[1] = "baz"; + + var text = target.Panel.Children + .OfType() + .Select(x => x.Text) + .ToList(); + + Assert.Equal(new[] { "foo", "baz", "baz" }, text); + } + + [Fact] + public void Moving_Items_Should_Update_Containers() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = new ItemsPresenter + { + Items = items, + }; + + target.ApplyTemplate(); + items.Move(2, 1); + + var text = target.Panel.Children + .OfType() + .Select(x => x.Text) + .ToList(); + + Assert.Equal(new[] { "foo", "baz", "bar" }, text); + } + + [Fact] + public void Setting_Items_To_Null_Should_Remove_Containers() { var target = new ItemsPresenter { @@ -87,6 +147,7 @@ namespace Perspex.Controls.UnitTests.Presenters target.Items = null; Assert.Empty(target.Panel.Children); + Assert.Empty(target.ItemContainerGenerator.Containers); } [Fact] @@ -102,12 +163,19 @@ namespace Perspex.Controls.UnitTests.Presenters target.ApplyTemplate(); var text = target.Panel.Children.Cast().Select(x => x.Text).ToList(); + Assert.Equal(new[] { "foo", "bar" }, text); + Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0)); + Assert.Null(target.ItemContainerGenerator.ContainerFromIndex(1)); + Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(2)); items.RemoveAt(1); text = target.Panel.Children.Cast().Select(x => x.Text).ToList(); + Assert.Equal(new[] { "foo", "bar" }, text); + Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0)); + Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(1)); } [Fact] @@ -231,7 +299,7 @@ namespace Perspex.Controls.UnitTests.Presenters { protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this); + return new ItemContainerGenerator(this, TestItem.ContentProperty); } } } diff --git a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs index b4cb191960..7308b016aa 100644 --- a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs @@ -1,36 +1,125 @@ // 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.Collections.Generic; using System.Linq; -using Perspex.Controls; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; +using Perspex.Input; using Perspex.LogicalTree; -using Perspex.Styling; using Xunit; namespace Perspex.Controls.UnitTests { public class TreeViewTests { + [Fact] + public void Items_Should_Be_Created() + { + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = CreateTestTreeData(), + DataTemplates = CreateNodeDataTemplate(), + }; + + target.ApplyTemplate(); + + Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); + Assert.Equal(new[] { "Child1", "Child2" }, ExtractItemHeader(target, 1)); + Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2)); + } + + [Fact] + public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() + { + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = CreateTestTreeData(), + DataTemplates = CreateNodeDataTemplate(), + }; + + target.ApplyTemplate(); + + var container = (TreeViewItem)target.ItemContainerGenerator.Containers.Single(); + var header = (TextBlock)container.Header; + Assert.Equal("Root", header.Text); + } + + [Fact] + public void Root_TreeContainerFromItem_Should_Return_Descendent_Item() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + DataTemplates = CreateNodeDataTemplate(), + }; + + // For TreeViewItem to find its parent TreeView, OnAttachedToVisualTree needs + // to be called, which requires an IRenderRoot. + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + ApplyTemplates(target); + + var container = target.ItemContainerGenerator.TreeContainerFromItem( + tree[0].Children[1].Children[0]); + var header = ((TreeViewItem)container).Header; + var headerContent = ((TextBlock)header).Text; + + Assert.Equal("Grandchild2a", headerContent); + } + + [Fact] + public void Clicking_Item_Should_Select_It() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + DataTemplates = CreateNodeDataTemplate(), + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + ApplyTemplates(target); + + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.TreeContainerFromItem(item); + + container.RaiseEvent(new PointerPressEventArgs + { + RoutedEvent = InputElement.PointerPressedEvent, + MouseButton = MouseButton.Left, + }); + + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); + } + [Fact] public void LogicalChildren_Should_Be_Set() { var target = new TreeView { - Template = new FuncControlTemplate(CreateTreeViewTemplate), + Template = CreateTreeViewTemplate(), Items = new[] { "Foo", "Bar", "Baz " }, }; target.ApplyTemplate(); - Assert.Equal(3, target.GetLogicalChildren().Count()); + var result = target.GetLogicalChildren() + .OfType() + .Select(x => x.Header) + .OfType() + .Select(x => x.Text) + .ToList(); - foreach (var child in target.GetLogicalChildren()) - { - Assert.IsType(child); - } + Assert.Equal(new[] { "Foo", "Bar", "Baz " }, result); } [Fact] @@ -39,18 +128,18 @@ namespace Perspex.Controls.UnitTests var items = new object[] { "Foo", - new Item("Bar"), + new Node { Value = "Bar" }, new TextBlock { Text = "Baz" }, new TreeViewItem { Header = "Qux" }, }; var target = new TreeView { - Template = new FuncControlTemplate(CreateTreeViewTemplate), + Template = CreateTreeViewTemplate(), DataContext = "Base", DataTemplates = new DataTemplates { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate(x => new Button { Content = x }) }, Items = items, }; @@ -67,35 +156,116 @@ namespace Perspex.Controls.UnitTests dataContexts); } - private Control CreateTreeViewTemplate(ITemplatedControl parent) + private void ApplyTemplates(TreeView tree) { - return new ScrollViewer + tree.ApplyTemplate(); + ApplyTemplates(tree.Presenter.Panel.Children); + } + + private void ApplyTemplates(IEnumerable controls) + { + foreach (TreeViewItem control in controls) { - Template = new FuncControlTemplate(CreateScrollViewerTemplate), - Content = new ItemsPresenter + control.Template = CreateTreeViewItemTemplate(); + control.ApplyTemplate(); + ApplyTemplates(control.Presenter.Panel.Children); + } + } + + private IList CreateTestTreeData() + { + return new[] + { + new Node { - Name = "itemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty), + Value = "Root", + Children = new[] + { + new Node + { + Value = "Child1", + }, + new Node + { + Value = "Child2", + Children = new[] + { + new Node + { + Value = "Grandchild2a", + }, + }, + }, + } } }; } - private Control CreateScrollViewerTemplate(ITemplatedControl parent) + private DataTemplates CreateNodeDataTemplate() { - return new ScrollContentPresenter + return new DataTemplates { - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty), + new FuncTreeDataTemplate( + x => new TextBlock { Text = x.Value }, + x => x.Children), }; } - private class Item + private IControlTemplate CreateTreeViewTemplate() { - public Item(string value) + return new FuncControlTemplate(parent => new ItemsPresenter { - Value = value; + Name = "itemsPresenter", + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + }); + } + + private IControlTemplate CreateTreeViewItemTemplate() + { + return new FuncControlTemplate(parent => new ItemsPresenter + { + Name = "itemsPresenter", + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + }); + } + + private List ExtractItemHeader(TreeView tree, int level) + { + return ExtractItemContent(tree.Presenter.Panel, 0, level) + .Select(x => x.Header) + .OfType() + .Select(x => x.Text) + .ToList(); + } + + private IEnumerable ExtractItemContent(IPanel panel, int currentLevel, int level) + { + foreach (TreeViewItem container in panel.Children) + { + if (container.Template == null) + { + container.Template = CreateTreeViewItemTemplate(); + container.ApplyTemplate(); + } + + if (currentLevel == level) + { + yield return container; + } + else + { + foreach (var child in ExtractItemContent(container.Presenter.Panel, currentLevel + 1, level)) + { + yield return child; + } + } } + } - public string Value { get; } + private class Node + { + public string Value { get; set; } + public IList Children { get; set; } } } }