Browse Source

Merge branch 'refs/heads/property-registry' into xaml-control-themes

Conflicts:
	tests/Perspex.Controls.UnitTests/TreeViewTests.cs
pull/297/head
Steven Kirk 11 years ago
parent
commit
88f155cdaa
  1. 6
      nuget/build-appveyor.ps1
  2. 11
      nuget/build-version.ps1
  3. 29
      nuget/template/Perspex.Desktop.nuspec
  4. 5
      nuget/template/Perspex.nuspec
  5. 11
      nuget/template/build/net45/perspex.targets
  6. 2
      src/Markup/Perspex.Markup.Xaml/Binding/XamlTemplateBinding.cs
  7. 4
      src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs
  8. 4
      src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyTypeConverter.cs
  9. 1
      src/Perspex.Base/Perspex.Base.csproj
  10. 172
      src/Perspex.Base/PerspexObject.cs
  11. 44
      src/Perspex.Base/PerspexObjectExtensions.cs
  12. 65
      src/Perspex.Base/PerspexProperty.cs
  13. 255
      src/Perspex.Base/PerspexPropertyRegistry.cs
  14. 34
      src/Perspex.Base/PerspexProperty`1.cs
  15. 6
      src/Perspex.Controls/Control.cs
  16. 2
      src/Perspex.Controls/DropDown.cs
  17. 25
      src/Perspex.Controls/Generators/IItemContainerGenerator.cs
  18. 22
      src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs
  19. 116
      src/Perspex.Controls/Generators/ItemContainerGenerator.cs
  20. 18
      src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs
  21. 260
      src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  22. 3
      src/Perspex.Controls/ListBox.cs
  23. 4
      src/Perspex.Controls/Presenters/CarouselPresenter.cs
  24. 34
      src/Perspex.Controls/Presenters/ItemsPresenter.cs
  25. 12
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  26. 2
      src/Perspex.Controls/Primitives/TabStrip.cs
  27. 172
      src/Perspex.Controls/TreeView.cs
  28. 39
      src/Perspex.Controls/TreeViewItem.cs
  29. 2
      src/Perspex.Diagnostics/Debug.cs
  30. 2
      src/Perspex.Diagnostics/ViewModels/ControlDetailsViewModel.cs
  31. 6
      src/Perspex.Layout/Layoutable.cs
  32. 10
      src/Perspex.Themes.Default/TreeViewItemStyle.cs
  33. 1
      tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj
  34. 24
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs
  35. 177
      tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs
  36. 18
      tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs
  37. 93
      tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
  38. 28
      tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
  39. 2
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  40. 2
      tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs
  41. 72
      tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  42. 220
      tests/Perspex.Controls.UnitTests/TreeViewTests.cs

6
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
}
}

11
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
nuget.exe pack Perspex.Desktop\Perspex.Desktop.nuspec
rm -Force -Recurse .\Perspex
rm -Force -Recurse .\Perspex.Desktop

29
nuget/template/Perspex.Desktop.nuspec

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<package>
<metadata>
<id>Perspex.Desktop</id>
<version>#VERSION#</version>
<authors>Perspex Team</authors>
<owners>stevenk</owners>
<licenseUrl>http://opensource.org/licenses/MIT</licenseUrl>
<projectUrl>https://github.com/Perspex/Perspex/</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>The Perspex UI framework</description>
<releaseNotes></releaseNotes>
<copyright>Copyright 2015</copyright>
<tags>Perspex</tags>
<dependencies>
<dependency id="Serilog" version="1.5.9" />
<dependency id="Splat" version="1.6.2" />
<dependency id="Sprache" version="2.0.0.47" />
<dependency id="Rx-Core" version="2.2.5" />
<dependency id="Rx-Interfaces" version="2.2.5" />
<dependency id="Rx-Linq" version="2.2.5" />
<dependency id="Rx-Main" version="2.2.5" />
<dependency id="Rx-PlatformServices" version="2.2.5" />
<dependency id="SharpDX" version="2.6.3"/>
<dependency id="SharpDX.Direct2D1" version="2.6.3"/>
<dependency id="SharpDX.DXGI" version="2.6.3"/>
</dependencies>
</metadata>
</package>

5
nuget/template/Perspex.nuspec

@ -3,10 +3,10 @@
<metadata>
<id>Perspex</id>
<version>#VERSION#</version>
<authors>stevenk</authors>
<authors>Perspex Team</authors>
<owners>stevenk</owners>
<licenseUrl>http://opensource.org/licenses/MIT</licenseUrl>
<projectUrl>https://github.com/grokys/Perspex/</projectUrl>
<projectUrl>https://github.com/Perspex/Perspex/</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>The Perspex UI framework</description>
<releaseNotes>Initial alpha release.</releaseNotes>
@ -21,6 +21,7 @@
<dependency id="Rx-Linq" version="2.2.5" />
<dependency id="Rx-Main" version="2.2.5" />
<dependency id="Rx-PlatformServices" version="2.2.5" />
<dependency id="Perspex" version="#VERSION#" />
</dependencies>
</metadata>
</package>

11
nuget/template/build/net45/perspex.targets

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Condition="'$(MSBuildThisFileDirectory)' != '' And HasTrailingSlash('$(MSBuildThisFileDirectory)')">
<PlatformLibs Include="$(MSBuildThisFileDirectory)**\*.dll" />
<Content Include="@(PlatformLibs)">
<Link>%(RecursiveDir)%(FileName)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

2
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)
{

4
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);
}

4
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);
}

1
src/Perspex.Base/Perspex.Base.csproj

@ -50,6 +50,7 @@
<Compile Include="PerspexLocator.cs" />
<Compile Include="Metadata\XmlnsDefinitionAttribute.cs" />
<Compile Include="PerspexObjectExtensions.cs" />
<Compile Include="PerspexPropertyRegistry.cs" />
<Compile Include="PerspexProperty`1.cs" />
<Compile Include="Platform\IPclPlatformWrapper.cs" />
<Compile Include="BindingPriority.cs" />

172
src/Perspex.Base/PerspexObject.cs

@ -25,18 +25,6 @@ namespace Perspex
/// </remarks>
public class PerspexObject : IObservablePropertyBag, INotifyPropertyChanged
{
/// <summary>
/// The registered properties by type.
/// </summary>
private static readonly Dictionary<Type, List<PerspexProperty>> s_registered =
new Dictionary<Type, List<PerspexProperty>>();
/// <summary>
/// The registered attached properties by owner type.
/// </summary>
private static readonly Dictionary<Type, List<PerspexProperty>> s_attached =
new Dictionary<Type, List<PerspexProperty>>();
/// <summary>
/// The parent object that inherited values are inherited from.
/// </summary>
@ -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
}
}
/// <summary>
/// Gets all <see cref="PerspexProperty"/>s registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
public static IEnumerable<PerspexProperty> GetRegisteredProperties(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
TypeInfo i = type.GetTypeInfo();
while (type != null)
{
List<PerspexProperty> list;
if (s_registered.TryGetValue(type, out list))
{
foreach (PerspexProperty p in list)
{
yield return p;
}
}
type = type.GetTypeInfo().BaseType;
}
}
/// <summary>
/// Gets all attached <see cref="PerspexProperty"/>s registered by an owner.
/// </summary>
/// <param name="ownerType">The owner type.</param>
/// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
public static IEnumerable<PerspexProperty> GetAttachedProperties(Type ownerType)
{
List<PerspexProperty> list;
if (s_attached.TryGetValue(ownerType, out list))
{
return list;
}
return Enumerable.Empty<PerspexProperty>();
}
/// <summary>
/// Registers a <see cref="PerspexProperty"/> on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="property">The property.</param>
/// <remarks>
/// You won't usually want to call this method directly, instead use the
/// <see cref="PerspexProperty.Register"/> method.
/// </remarks>
public static void Register(Type type, PerspexProperty property)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(property != null);
List<PerspexProperty> list;
if (!s_registered.TryGetValue(type, out list))
{
list = new List<PerspexProperty>();
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<PerspexProperty>();
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
}
}
/// <summary>
/// Gets all properties that are registered on this object.
/// </summary>
/// <returns>
/// A collection of <see cref="PerspexProperty"/> objects.
/// </returns>
public IEnumerable<PerspexProperty> GetRegisteredProperties()
{
return GetRegisteredProperties(GetType());
}
/// <summary>
/// Checks whether a <see cref="PerspexProperty"/> is set on this object.
/// </summary>
@ -478,16 +369,6 @@ namespace Perspex
return false;
}
/// <summary>
/// Checks whether a <see cref="PerspexProperty"/> is registered on this class.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>True if the property is registered, otherwise false.</returns>
public bool IsRegistered(PerspexProperty property)
{
return FindRegistered(property) != null;
}
/// <summary>
/// Sets a <see cref="PerspexProperty"/> value.
/// </summary>
@ -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
}
}
/// <inheritdoc/>
bool IPropertyBag.IsRegistered(PerspexProperty property)
{
return PerspexPropertyRegistry.Instance.IsRegistered(this, property);
}
/// <summary>
/// Gets all priority values set on the object.
/// </summary>
@ -936,41 +823,6 @@ namespace Perspex
}
}
/// <summary>
/// Given a <see cref="PerspexProperty"/> returns a registered perspex property that is
/// equal.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The registered property or null if not found.</returns>
/// <remarks>
/// 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 <see cref="object.Equals(object)"/>.
/// </remarks>
public PerspexProperty FindRegistered(PerspexProperty property)
{
Type type = GetType();
while (type != null)
{
List<PerspexProperty> 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;
}
/// <summary>
/// Given a <see cref="PerspexProperty"/> returns a registered perspex property that is
/// equal or throws if not found.
@ -979,7 +831,7 @@ namespace Perspex
/// <returns>The registered property.</returns>
public PerspexProperty GetRegistered(PerspexProperty property)
{
var result = FindRegistered(property);
var result = PerspexPropertyRegistry.Instance.FindRegistered(this, property);
if (result == null)
{

44
src/Perspex.Base/PerspexObjectExtensions.cs

@ -52,50 +52,6 @@ namespace Perspex
return observable.Subscribe(e => SubscribeAdapter(e, handler));
}
/// <summary>
/// Finds a registered property on a <see cref="PerspexObject"/> by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="name">
/// The property name. If an attached property it should be in the form
/// "OwnerType.PropertyName".
/// </param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
public static PerspexProperty FindRegistered(this PerspexObject o, string name)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(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]);
}
}
/// <summary>
/// Observer method for <see cref="AddClassHandler{TTarget}(IObservable{PerspexPropertyChangedEventArgs},
/// Func{TTarget, Action{PerspexPropertyChangedEventArgs}})"/>.

65
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;
/// <summary>
/// The default values for the property, by type.
/// The default value provided when the property was first registered.
/// </summary>
private readonly object _defaultValue;
/// <summary>
/// The overridden default values for the property, by type.
/// </summary>
private readonly Dictionary<Type, object> _defaultValues = new Dictionary<Type, object>();
@ -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 <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="source">The direct property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
protected PerspexProperty(PerspexProperty source, Type ownerType)
{
Contract.Requires<ArgumentNullException>(source != null);
Contract.Requires<ArgumentNullException>(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;
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="source">The direct property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
/// <param name="getter">A new getter.</param>
/// <param name="setter">A new setter.</param>
protected PerspexProperty(
PerspexProperty source,
Type ownerType,
Func<PerspexObject, object> getter,
Action<PerspexObject, object> setter)
{
Contract.Requires<ArgumentNullException>(source != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<ArgumentNullException>(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;
}
/// <summary>

255
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
{
/// <summary>
/// Tracks registered <see cref="PerspexProperty"/> instances.
/// </summary>
public class PerspexPropertyRegistry
{
/// <summary>
/// The registered properties by type.
/// </summary>
private readonly Dictionary<Type, List<PerspexProperty>> _registered =
new Dictionary<Type, List<PerspexProperty>>();
/// <summary>
/// The registered attached properties by owner type.
/// </summary>
private readonly Dictionary<Type, List<PerspexProperty>> _attached =
new Dictionary<Type, List<PerspexProperty>>();
/// <summary>
/// Gets the <see cref="PerspexPropertyRegistry"/> instance
/// </summary>
public static PerspexPropertyRegistry Instance { get; }
= new PerspexPropertyRegistry();
/// <summary>
/// Gets all attached <see cref="PerspexProperty"/>s registered by an owner.
/// </summary>
/// <param name="ownerType">The owner type.</param>
/// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
public IEnumerable<PerspexProperty> GetAttached(Type ownerType)
{
List<PerspexProperty> list;
if (_attached.TryGetValue(ownerType, out list))
{
return list;
}
return Enumerable.Empty<PerspexProperty>();
}
/// <summary>
/// Gets all <see cref="PerspexProperty"/>s registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
public IEnumerable<PerspexProperty> GetRegistered(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
var i = type.GetTypeInfo();
while (type != null)
{
List<PerspexProperty> list;
if (_registered.TryGetValue(type, out list))
{
foreach (PerspexProperty p in list)
{
yield return p;
}
}
type = type.GetTypeInfo().BaseType;
}
}
/// <summary>
/// Gets all <see cref="PerspexProperty"/>s registered on a object.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
public IEnumerable<PerspexProperty> GetRegistered(PerspexObject o)
{
Contract.Requires<ArgumentNullException>(o != null);
return GetRegistered(o.GetType());
}
/// <summary>
/// Finds <see cref="PerspexProperty"/> registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="property">The property.</param>
/// <returns>The registered property or null if not found.</returns>
/// <remarks>
/// Calling AddOwner on a PerspexProperty creates a new PerspexProperty that is a
/// different object but is equal according to <see cref="object.Equals(object)"/>.
/// </remarks>
public PerspexProperty FindRegistered(Type type, PerspexProperty property)
{
while (type != null)
{
List<PerspexProperty> list;
if (_registered.TryGetValue(type, out list))
{
var index = list.IndexOf(property);
if (index != -1)
{
return list[index];
}
}
type = type.GetTypeInfo().BaseType;
}
return null;
}
/// <summary>
/// Finds <see cref="PerspexProperty"/> registered on an object.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <returns>The registered property or null if not found.</returns>
/// <remarks>
/// Calling AddOwner on a PerspexProperty creates a new PerspexProperty that is a
/// different object but is equal according to <see cref="object.Equals(object)"/>.
/// </remarks>
public PerspexProperty FindRegistered(object o, PerspexProperty property)
{
return FindRegistered(o.GetType(), property);
}
/// <summary>
/// Finds a registered property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="name">
/// The property name. If an attached property it should be in the form
/// "OwnerType.PropertyName".
/// </param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
public PerspexProperty FindRegistered(Type type, string name)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(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<string> GetImplementedTypes(Type type)
{
while (type != null)
{
yield return type.Name;
type = type.GetTypeInfo().BaseType;
}
}
/// <summary>
/// Checks whether a <see cref="PerspexProperty"/> is registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="property">The property.</param>
/// <returns>True if the property is registered, otherwise false.</returns>
public bool IsRegistered(Type type, PerspexProperty property)
{
return FindRegistered(type, property) != null;
}
/// <summary>
/// Checks whether a <see cref="PerspexProperty"/> is registered on a object.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <returns>True if the property is registered, otherwise false.</returns>
public bool IsRegistered(object o, PerspexProperty property)
{
return IsRegistered(o.GetType(), property);
}
/// <summary>
/// Registers a <see cref="PerspexProperty"/> on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="property">The property.</param>
/// <remarks>
/// You won't usually want to call this method directly, instead use the
/// <see cref="PerspexProperty.Register"/> method.
/// </remarks>
public void Register(Type type, PerspexProperty property)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(property != null);
List<PerspexProperty> list;
if (!_registered.TryGetValue(type, out list))
{
list = new List<PerspexProperty>();
_registered.Add(type, list);
}
if (!list.Contains(property))
{
list.Add(property);
}
if (property.IsAttached)
{
if (!_attached.TryGetValue(property.OwnerType, out list))
{
list = new List<PerspexProperty>();
_attached.Add(property.OwnerType, list);
}
if (!list.Contains(property))
{
list.Add(property);
}
}
}
}
}

34
src/Perspex.Base/PerspexProperty`1.cs

@ -66,17 +66,29 @@ namespace Perspex
Setter = setter;
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="source">The property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
private PerspexProperty(PerspexProperty source, Type ownerType)
: base(source, ownerType)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="source">The direct property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
/// <param name="getter">A new getter.</param>
/// <param name="setter">A new setter.</param>
private PerspexProperty(
PerspexProperty source,
Type ownerType,
Func<PerspexObject, TValue> getter,
Action<PerspexObject, TValue> 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<TValue>(this, typeof(TOwner));
PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}
/// <summary>
@ -119,8 +132,19 @@ namespace Perspex
Action<TOwner, TValue> setter = null)
where TOwner : PerspexObject
{
var result = new PerspexProperty<TValue>(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<TValue>(
this,
typeof(TOwner),
CastReturn(getter),
CastParam1(setter));
PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}

6
src/Perspex.Controls/Control.cs

@ -406,7 +406,11 @@ namespace Perspex.Controls
base.OnAttachedToVisualTree(root);
IStyler styler = PerspexLocator.Current.GetService<IStyler>();
styler.ApplyStyles(this);
if (styler != null)
{
styler.ApplyStyles(this);
}
}
/// <summary>

2
src/Perspex.Controls/DropDown.cs

@ -80,7 +80,7 @@ namespace Perspex.Controls
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<ListBoxItem>(this);
return new ItemContainerGenerator<ListBoxItem>(this, ListBoxItem.ContentProperty);
}
protected override void OnKeyDown(KeyEventArgs e)

25
src/Perspex.Controls/Generators/IItemContainerGenerator.cs

@ -32,26 +32,37 @@ namespace Perspex.Controls.Generators
/// <param name="items">The items.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created controls.</returns>
IList<IControl> CreateContainers(
IEnumerable<IControl> Materialize(
int startingIndex,
IEnumerable items,
IMemberSelector selector);
/// <summary>
/// Removes a set of created containers from the index and returns the removed controls.
/// Removes a set of created containers.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// </param>
/// <param name="items">The items.</param>
/// <returns>The removed controls.</returns>
IList<IControl> RemoveContainers(int startingIndex, IEnumerable items);
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<IControl> Dematerialize(int startingIndex, int count);
/// <summary>
/// Removes a set of created containers and updates the index of later containers to fill
/// the gap.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<IControl> RemoveRange(int startingIndex, int count);
/// <summary>
/// Clears the created containers from the index and returns the removed controls.
/// Clears all created containers and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
IList<IControl> ClearContainers();
IEnumerable<IControl> Clear();
/// <summary>
/// Gets the container control representing the item with the specified index.

22
src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs

@ -11,23 +11,23 @@ namespace Perspex.Controls.Generators
public interface ITreeItemContainerGenerator : IItemContainerGenerator
{
/// <summary>
/// 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.
/// </summary>
/// <returns>The containers.</returns>
IEnumerable<IControl> GetAllContainers();
ITreeItemContainerGenerator RootGenerator { get; }
/// <summary>
/// Gets the item that is contained by the specified container.
/// Gets the item container for the specified item, anywhere in the tree.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The item.</returns>
object ItemFromContainer(IControl container);
/// <param name="item">The item.</param>
/// <returns>The container, or null if not found.</returns>
IControl TreeContainerFromItem(object item);
/// <summary>
/// Gets the container for the specified item
/// Gets the item for the specified item container, anywhere in the tree.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The container.</returns>
IControl ContainerFromItem(object item);
/// <param name="container">The container.</param>
/// <returns>The item, or null if not found.</returns>
object TreeItemFromContainer(IControl container);
}
}

116
src/Perspex.Controls/Generators/ItemContainerGenerator.cs

@ -15,7 +15,7 @@ namespace Perspex.Controls.Generators
/// </summary>
public class ItemContainerGenerator : IItemContainerGenerator
{
private Dictionary<int, IControl> _containers = new Dictionary<int, IControl>();
private List<IControl> _containers = new List<IControl>();
private readonly Subject<ItemContainers> _containersInitialized = new Subject<ItemContainers>();
@ -25,17 +25,15 @@ namespace Perspex.Controls.Generators
/// <param name="owner">The owner control.</param>
public ItemContainerGenerator(IControl owner)
{
Contract.Requires<ArgumentNullException>(owner != null);
Owner = owner;
}
/// <summary>
/// Gets the currently realized containers.
/// </summary>
public IEnumerable<IControl> Containers => _containers.Values;
/// <inheritdoc/>
public IEnumerable<IControl> Containers => _containers;
/// <summary>
/// Signalled whenever new containers are initialized.
/// </summary>
/// <inheritdoc/>
public IObservable<ItemContainers> ContainersInitialized => _containersInitialized;
/// <summary>
@ -43,16 +41,8 @@ namespace Perspex.Controls.Generators
/// </summary>
public IControl Owner { get; }
/// <summary>
/// Creates container controls for a collection of items.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// </param>
/// <param name="items">The items.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created container controls.</returns>
public IList<IControl> CreateContainers(
/// <inheritdoc/>
public IEnumerable<IControl> Materialize(
int startingIndex,
IEnumerable items,
IMemberSelector selector)
@ -75,72 +65,54 @@ namespace Perspex.Controls.Generators
return result.Where(x => x != null).ToList();
}
/// <summary>
/// Removes a set of created containers from the index and returns the removed controls.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// </param>
/// <param name="items">The items.</param>
/// <returns>The removed controls.</returns>
public IList<IControl> RemoveContainers(int startingIndex, IEnumerable items)
/// <inheritdoc/>
public virtual IEnumerable<IControl> Dematerialize(int startingIndex, int count)
{
var result = new List<IControl>();
var count = items.Cast<object>().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;
}
/// <summary>
/// Clears the created containers from the index and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
public IList<IControl> ClearContainers()
/// <inheritdoc/>
public virtual IEnumerable<IControl> RemoveRange(int startingIndex, int count)
{
var result = _containers;
_containers = new Dictionary<int, IControl>();
return result.Values.ToList();
var result = _containers.GetRange(startingIndex, count);
_containers.RemoveRange(startingIndex, count);
return result;
}
/// <summary>
/// Gets the container control representing the item with the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The container or null if no container created.</returns>
public IControl ContainerFromIndex(int index)
/// <inheritdoc/>
public virtual IEnumerable<IControl> Clear()
{
IControl result;
_containers.TryGetValue(index, out result);
var result = _containers;
_containers = new List<IControl>();
return result;
}
/// <summary>
/// Gets the index of the specified container control.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The index of the container or -1 if not found.</returns>
public int IndexFromContainer(IControl container)
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public int IndexFromContainer(IControl container)
{
return _containers.IndexOf(container);
}
/// <summary>
@ -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;
}
}
/// <summary>
/// Gets all containers with an index that fall within a range.
/// </summary>
/// <param name="index">The first index.</param>
/// <param name="count">The number of elements in the range.</param>
/// <returns>The containers.</returns>
protected IEnumerable<IControl> GetContainerRange(int index, int count)
{
return _containers.GetRange(index, count);
}
}
}

18
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.
/// </summary>
/// <typeparam name="T">The type of the container.</typeparam>
public class ItemContainerGenerator<T> : ItemContainerGenerator where T : class, IContentControl, new()
public class ItemContainerGenerator<T> : ItemContainerGenerator where T : class, IControl, new()
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public ItemContainerGenerator(Control owner)
/// <param name="contentProperty">The container's Content property.</param>
public ItemContainerGenerator(
IControl owner,
PerspexProperty contentProperty)
: base(owner)
{
ContentProperty = contentProperty;
}
/// <summary>
/// Gets the container's Content property.
/// </summary>
protected PerspexProperty ContentProperty { get; }
/// <inheritdoc/>
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))
{

260
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.
/// </summary>
/// <typeparam name="T">The type of the container.</typeparam>
public class TreeItemContainerGenerator<T> : ITreeItemContainerGenerator where T : TreeViewItem, new()
public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
where T : class, IControl, new()
{
private Dictionary<object, T> _containers = new Dictionary<object, T>();
private readonly Subject<ItemContainers> _containersInitialized = new Subject<ItemContainers>();
private Dictionary<object, T> _itemToContainer;
private Dictionary<IControl, object> _containerToItem;
/// <summary>
/// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public TreeItemContainerGenerator(IControl owner)
{
Owner = owner;
}
/// <summary>
/// Gets the currently realized containers.
/// </summary>
public IEnumerable<IControl> Containers => _containers.Values;
/// <summary>
/// Signalled whenever new containers are initialized.
/// </summary>
public IObservable<ItemContainers> ContainersInitialized => _containersInitialized;
/// <summary>
/// Gets the owner control.
/// </summary>
public IControl Owner { get; }
/// <summary>
/// Creates container controls for a collection of items.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// </param>
/// <param name="items">The items.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created container controls.</returns>
public IList<IControl> CreateContainers(
int startingIndex,
IEnumerable items,
IMemberSelector selector)
{
Contract.Requires<ArgumentNullException>(items != null);
int index = startingIndex;
var result = new List<IControl>();
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();
}
/// <summary>
/// Removes a set of created containers from the index and returns the removed controls.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// <param name="contentProperty">The container's Content property.</param>
/// <param name="itemsProperty">The container's Items property.</param>
/// <param name="isExpandedProperty">The container's IsExpanded property.</param>
/// <param name="rootGenerator">
/// The item container for the root of the tree, or null if this generator is itself the
/// root of the tree.
/// </param>
/// <param name="items">The items.</param>
/// <returns>The removed controls.</returns>
public IList<IControl> RemoveContainers(int startingIndex, IEnumerable items)
public TreeItemContainerGenerator(
IControl owner,
PerspexProperty contentProperty,
PerspexProperty itemsProperty,
PerspexProperty isExpandedProperty,
ITreeItemContainerGenerator rootGenerator)
: base(owner, contentProperty)
{
var result = new List<IControl>();
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<object, T>();
_containerToItem = new Dictionary<IControl, object>();
}
return result;
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The removed controls.</returns>
public IList<IControl> ClearContainers()
{
var result = _containers;
_containers = new Dictionary<object, T>();
return result.Values.Cast<IControl>().ToList();
}
public ITreeItemContainerGenerator RootGenerator { get; }
/// <summary>
/// Gets the container control representing the item with the specified index.
/// Gets the item container's Items property.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The container or null if no container created.</returns>
public IControl ContainerFromIndex(int index)
{
throw new NotImplementedException();
}
protected PerspexProperty ItemsProperty { get; }
/// <summary>
/// Gets the index of the specified container control.
/// Gets the item container's IsExpanded property.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The index of the container or -1 if not found.</returns>
public int IndexFromContainer(IControl container)
{
throw new NotImplementedException();
}
protected PerspexProperty IsExpandedProperty { get; }
/// <summary>
/// Gets all of the generated container controls.
/// Gets the item container for the specified item, anywhere in the tree.
/// </summary>
/// <returns>The containers.</returns>
public IEnumerable<IControl> GetAllContainers()
/// <param name="item">The item.</param>
/// <returns>The container, or null if not found.</returns>
public IControl TreeContainerFromItem(object item)
{
return _containers.Values;
T result;
_itemToContainer.TryGetValue(item, out result);
return result;
}
/// <summary>
/// Gets the item that is contained by the specified container.
/// Gets the item for the specified item container, anywhere in the tree.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The item.</returns>
public object ItemFromContainer(IControl container)
{
return container.DataContext;
}
/// <summary>
/// Gets the container for the specified item
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The container.</returns>
public IControl ContainerFromItem(object item)
/// <returns>The item, or null if not found.</returns>
public object TreeItemFromContainer(IControl container)
{
T result;
_containers.TryGetValue(item, out result);
object result;
_containerToItem.TryGetValue(container, out result);
return result;
}
/// <summary>
/// Creates the container for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The created container control.</returns>
protected virtual T CreateContainer(object item)
/// <inheritdoc/>
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<IControl> Clear()
{
ClearIndex();
return base.Clear();
}
public override IEnumerable<IControl> 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<T>)RootGenerator).AddToIndex(item, container);
}
else
{
_itemToContainer.Add(item, container);
_containerToItem.Add(container, item);
}
}
private void RemoveFromIndex(IEnumerable<IControl> containers)
{
if (RootGenerator != null)
{
((TreeItemContainerGenerator<T>)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<T>)RootGenerator).ClearIndex();
}
else
{
_containerToItem.Clear();
_itemToContainer.Clear();
}
}
/// <summary>
/// Gets the data template for the specified item.
/// </summary>
@ -222,29 +198,5 @@ namespace Perspex.Controls.Generators
return treeTemplate;
}
private void Remove(T container, IList<IControl> 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);
}
}
}
}

3
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
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<ListBoxItem>(this);
return new ItemContainerGenerator<ListBoxItem>(this, ListBoxItem.ContentProperty);
}
/// <inheritdoc/>

4
src/Perspex.Controls/Presenters/CarouselPresenter.cs

@ -199,7 +199,7 @@ namespace Perspex.Controls.Presenters
if (toIndex != -1)
{
var item = Items.Cast<object>().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);
}
}

34
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<IControl> 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;
}

12
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
}
/// <summary>
/// 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.
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <param name="select">Whether the container should be selected or unselected.</param>
@ -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;
}

2
src/Perspex.Controls/Primitives/TabStrip.cs

@ -43,7 +43,7 @@ namespace Perspex.Controls.Primitives
}
else
{
result = new ItemContainerGenerator<TabItem>(this);
result = new ItemContainerGenerator<TabItem>(this, TabItem.ContentProperty);
}
return result;

172
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
{
/// <summary>
/// Displays a hierachical tree of data.
/// </summary>
public class TreeView : ItemsControl
{
/// <summary>
/// Defines the <see cref="SelectedItem"/> property.
/// </summary>
public static readonly PerspexProperty<object> SelectedItemProperty =
SelectingItemsControl.SelectedItemProperty.AddOwner<TreeView>(
o => o.SelectedItem,
@ -18,61 +27,174 @@ namespace Perspex.Controls
private object _selectedItem;
/// <summary>
/// Initializes static members of the <see cref="TreeView"/> class.
/// </summary>
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;
/// <summary>
/// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
/// </summary>
public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.ItemContainerGenerator;
/// <summary>
/// Gets or sets the selected item.
/// </summary>
public object SelectedItem
{
get { return _selectedItem; }
set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); }
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new TreeItemContainerGenerator<TreeViewItem>(this);
return new TreeItemContainerGenerator<TreeViewItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
null);
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
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);
}
}
/// <summary>
/// Updates the selection for an item based on user interaction.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="select">Whether the item should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
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)
/// <summary>
/// Updates the selection based on an event that may have originated in a container that
/// belongs to the control.
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <param name="select">Whether the container should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <returns>
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
/// </returns>
protected bool UpdateSelectionFromEventSource(
IInteractive eventSource,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
{
var containers = ItemContainerGenerator.GetAllContainers().OfType<ISelectable>();
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;
}
/// <summary>
/// Tries to get the container that was the source of an event.
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <returns>The container or null if the event did not originate in a container.</returns>
protected IControl GetContainerFromEventSource(IInteractive eventSource)
{
var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
.OfType<TreeViewItem>()
.FirstOrDefault();
if (item != null)
{
if (item.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator)
{
return item;
}
}
return null;
}
/// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="selected">Whether the control is selected</param>
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");
}
}
}
}

39
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); }
}
/// <summary>
/// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
/// </summary>
public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.ItemContainerGenerator;
/// <inheritdoc/>
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<TreeViewItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
_treeView?.ItemContainerGenerator);
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(IRenderRoot root)
{
base.OnAttachedToVisualTree(root);
if (this.GetVisualParent() != null)
{
_treeView = this.GetVisualAncestors().OfType<TreeView>().FirstOrDefault();
if (_treeView == null)
{
throw new InvalidOperationException("TreeViewItems must be added to a TreeView.");
}
}
else
{
_treeView = null;
}
_treeView = this.GetVisualAncestors().OfType<TreeView>().FirstOrDefault();
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
if (!e.Handled)

2
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);

2
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);

6
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);
}

10
src/Perspex.Themes.Default/TreeViewItemStyle.cs

@ -13,6 +13,7 @@ using Perspex.Styling;
namespace Perspex.Themes.Default
{
using Collections;
using Controls = Controls.Controls;
/// <summary>
@ -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<double>(1, 2),
[Grid.ColumnProperty] = 1,
[!Rectangle.IsVisibleProperty] = control[!TreeViewItem.IsFocusedProperty],
}
}
},

1
tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj

@ -75,6 +75,7 @@
<Compile Include="Collections\PropertyChangedTracker.cs" />
<Compile Include="PerspexObjectTests_Direct.cs" />
<Compile Include="PerspexObjectTests_GetObservable.cs" />
<Compile Include="PerspexPropertyRegistryTests.cs" />
<Compile Include="PerspexObjectTests_Validation.cs" />
<Compile Include="PerspexObjectTests_Binding.cs" />
<Compile Include="PerspexObjectTests_Inheritance.cs" />

24
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()
{

177
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<string> FooProperty =
PerspexProperty.Register<Class1, string>("Foo");
public static readonly PerspexProperty<string> BazProperty =
PerspexProperty.Register<Class1, string>("Baz");
public static readonly PerspexProperty<int> QuxProperty =
PerspexProperty.Register<Class1, int>("Qux");
}
private class Class2 : Class1
{
public static readonly PerspexProperty<string> BarProperty =
PerspexProperty.Register<Class2, string>("Bar");
public static readonly PerspexProperty<double> FlobProperty =
PerspexProperty.Register<Class2, double>("Flob");
public static readonly PerspexProperty<double?> FredProperty =
PerspexProperty.Register<Class2, double?>("Fred");
}
private class Class3 : Class1
{
public static readonly PerspexProperty<string> AttachedProperty =
AttachedOwner.AttachedProperty.AddOwner<Class3>();
}
public class Class4 : PerspexObject
{
}
private class AttachedOwner
{
public static readonly PerspexProperty<string> AttachedProperty =
PerspexProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached");
}
}
}

18
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<string>("p1", typeof(Class1));
var p2 = p1.AddOwner<Class3>();
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<string>("d1", typeof(Class1), o => null, (o, v) => { });
var p2 = p1.AddOwner<Class3>(o => null, (o, v) => { });
Assert.Equal(typeof(Class3), p2.OwnerType);
}
[Fact]
public void AddOwner_With_Getter_And_Setter_On_Standard_Property_Should_Throw()
{

93
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<TextBlock>().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);
}
}
}

28
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<ListBoxItem>(owner, ListBoxItem.ContentProperty);
var containers = target.Materialize(0, items, null);
var result = containers
.OfType<ListBoxItem>()
.Select(x => x.Content)
.OfType<TextBlock>()
.Select(x => x.Text).ToList();
Assert.Equal(items, result);
}
}
}

2
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -81,6 +81,8 @@
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="Generators\ItemContainerGeneratorTests.cs" />
<Compile Include="Generators\ItemContainerGeneratorTypedTests.cs" />
<Compile Include="GridLengthTests.cs" />
<Compile Include="ContentPresenterTests.cs" />
<Compile Include="BorderTests.cs" />

2
tests/Perspex.Controls.UnitTests/Presenters/CarouselPresenterTests.cs

@ -74,7 +74,7 @@ namespace Perspex.Controls.UnitTests.Presenters
{
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<TestItem>(this);
return new ItemContainerGenerator<TestItem>(this, TestItem.ContentProperty);
}
}
}

72
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<ListBoxItem>(target);
target.ItemContainerGenerator = new ItemContainerGenerator<ListBoxItem>(
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<string> { "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<string> { "foo", "bar", "baz" };
var target = new ItemsPresenter
{
Items = items,
};
target.ApplyTemplate();
items[1] = "baz";
var text = target.Panel.Children
.OfType<TextBlock>()
.Select(x => x.Text)
.ToList();
Assert.Equal(new[] { "foo", "baz", "baz" }, text);
}
[Fact]
public void Moving_Items_Should_Update_Containers()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new ItemsPresenter
{
Items = items,
};
target.ApplyTemplate();
items.Move(2, 1);
var text = target.Panel.Children
.OfType<TextBlock>()
.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<TextBlock>().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<TextBlock>().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<TestItem>(this);
return new ItemContainerGenerator<TestItem>(this, TestItem.ContentProperty);
}
}
}

220
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<TreeViewItem>()
.Select(x => x.Header)
.OfType<TextBlock>()
.Select(x => x.Text)
.ToList();
foreach (var child in target.GetLogicalChildren())
{
Assert.IsType<TreeViewItem>(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<Item>(x => new Button { Content = x })
new FuncDataTemplate<Node>(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<IControl> 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<Node> 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<Node>(
x => new TextBlock { Text = x.Value },
x => x.Children),
};
}
private class Item
private IControlTemplate CreateTreeViewTemplate()
{
public Item(string value)
return new FuncControlTemplate<TreeView>(parent => new ItemsPresenter
{
Value = value;
Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
});
}
private IControlTemplate CreateTreeViewItemTemplate()
{
return new FuncControlTemplate<TreeViewItem>(parent => new ItemsPresenter
{
Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
});
}
private List<string> ExtractItemHeader(TreeView tree, int level)
{
return ExtractItemContent(tree.Presenter.Panel, 0, level)
.Select(x => x.Header)
.OfType<TextBlock>()
.Select(x => x.Text)
.ToList();
}
private IEnumerable<TreeViewItem> 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<Node> Children { get; set; }
}
}
}

Loading…
Cancel
Save