Browse Source

Merge branch 'bindings' into xaml-datatemplates

Conflicts:
	samples/XamlTestApplication/Views/MainWindow.paml
pull/237/head
Steven Kirk 11 years ago
parent
commit
c050547273
  1. 14
      Perspex.sln
  2. 8
      samples/TestApplication/Program.cs
  3. 1
      samples/XamlTestApplication/Views/MainWindow.paml
  4. 2
      src/Gtk/Perspex.Cairo/Media/DrawingContext.cs
  5. 4
      src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs
  6. 12
      src/Gtk/Perspex.Gtk/WindowImpl.cs
  7. 32
      src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs
  8. 30
      src/Markup/Perspex.Markup.Xaml/Converters/KeyGestureConverter.cs
  9. 1
      src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj
  10. 102
      src/Markup/Perspex.Markup/Binding/ExpressionNode.cs
  11. 29
      src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs
  12. 88
      src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs
  13. 24
      src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs
  14. 38
      src/Markup/Perspex.Markup/Binding/ExpressionValue.cs
  15. 106
      src/Markup/Perspex.Markup/Binding/IndexerNode.cs
  16. 40
      src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs
  17. 67
      src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs
  18. 125
      src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs
  19. 51
      src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs
  20. 34
      src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs
  21. 44
      src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs
  22. 140
      src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs
  23. 81
      src/Markup/Perspex.Markup/Perspex.Markup.csproj
  24. 32
      src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs
  25. 8
      src/Markup/Perspex.Markup/packages.config
  26. 3
      src/Perspex.Application/Application.cs
  27. 47
      src/Perspex.Base/BindingPriority.cs
  28. 30
      src/Perspex.Base/Metadata/XmlnsDefinitionAttribute.cs
  29. 1
      src/Perspex.Base/Perspex.Base.csproj
  30. 434
      src/Perspex.Base/PerspexObject.cs
  31. 221
      src/Perspex.Base/PerspexProperty.cs
  32. 123
      src/Perspex.Base/PerspexProperty`1.cs
  33. 4
      src/Perspex.Base/PriorityValue.cs
  34. 2
      src/Perspex.Base/Utilities/TypeUtilities.cs
  35. 2
      src/Perspex.Controls/Border.cs
  36. 12
      src/Perspex.Controls/Button.cs
  37. 26
      src/Perspex.Controls/Control.cs
  38. 12
      src/Perspex.Controls/DropDown.cs
  39. 117
      src/Perspex.Controls/HotkeyManager.cs
  40. 53
      src/Perspex.Controls/ItemsControl.cs
  41. 10
      src/Perspex.Controls/Menu.cs
  42. 13
      src/Perspex.Controls/MenuItem.cs
  43. 2
      src/Perspex.Controls/Perspex.Controls.csproj
  44. 2
      src/Perspex.Controls/Platform/ITopLevelImpl.cs
  45. 17
      src/Perspex.Controls/Presenters/ContentPresenter.cs
  46. 19
      src/Perspex.Controls/Presenters/DeckPresenter.cs
  47. 8
      src/Perspex.Controls/Presenters/ItemsPresenter.cs
  48. 64
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  49. 3
      src/Perspex.Controls/Properties/AssemblyInfo.cs
  50. 6
      src/Perspex.Controls/TextBlock.cs
  51. 18
      src/Perspex.Controls/TextBox.cs
  52. 38
      src/Perspex.Controls/TopLevel.cs
  53. 77
      src/Perspex.Controls/Utils/AncestorFinder.cs
  54. 2
      src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs
  55. 4
      src/Perspex.Input/IInputElement.cs
  56. 18
      src/Perspex.Input/InputElement.cs
  57. 2
      src/Perspex.Input/InputExtensions.cs
  58. 40
      src/Perspex.Input/KeyBinding.cs
  59. 116
      src/Perspex.Input/KeyGesture.cs
  60. 14
      src/Perspex.Input/KeyboardDevice.cs
  61. 2
      src/Perspex.Input/Perspex.Input.csproj
  62. 2
      src/Perspex.Input/Properties/AssemblyInfo.cs
  63. 18
      src/Perspex.Interactivity/Interactive.cs
  64. 14
      src/Perspex.Interactivity/RoutedEvent.cs
  65. 7
      src/Perspex.SceneGraph/Media/FormattedText.cs
  66. 2
      src/Perspex.SceneGraph/Media/IDrawingContext.cs
  67. 21
      src/Perspex.SceneGraph/Visual.cs
  68. 8
      src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs
  69. 2
      src/Perspex.Styling/LogicalTree/LogicalExtensions.cs
  70. 2
      src/Perspex.Styling/Styling/StyleBinding.cs
  71. 53
      src/Perspex.Themes.Default/TextBoxStyle.cs
  72. 2
      src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs
  73. 4
      src/Windows/Perspex.Direct2D1/Media/Imaging/BitmapImpl.cs
  74. 16
      src/Windows/Perspex.Win32/WindowImpl.cs
  75. 1
      tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj
  76. 4
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs
  77. 332
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs
  78. 13
      tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs
  79. 4
      tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs
  80. 65
      tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs
  81. 14
      tests/Perspex.Base.UnitTests/PriorityValueTests.cs
  82. 18
      tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs
  83. 2
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  84. 1
      tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs
  85. 5
      tests/Perspex.Controls.UnitTests/TabControlTests.cs
  86. 40
      tests/Perspex.Controls.UnitTests/Utils/AncestorFinderTests.cs
  87. 74
      tests/Perspex.Controls.UnitTests/Utils/HotKeyManagerTests.cs
  88. 28
      tests/Perspex.Input.UnitTests/KeyGestureParseTests.cs
  89. 1
      tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj
  90. 148
      tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs
  91. 74
      tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs
  92. 113
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs
  93. 97
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs
  94. 68
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs
  95. 242
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs
  96. 87
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs
  97. 42
      tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs
  98. 113
      tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj
  99. 36
      tests/Perspex.Markup.UnitTests/Properties/AssemblyInfo.cs
  100. 68
      tests/Perspex.Markup.UnitTests/UnitTestSynchronizationContext.cs

14
Perspex.sln

@ -96,6 +96,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.HtmlRenderer", "src
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup", "src\Markup\Perspex.Markup\Perspex.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup.UnitTests", "tests\Perspex.Markup.UnitTests\Perspex.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
@ -235,6 +239,14 @@ Global
{5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Release|Any CPU.Build.0 = Release|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.Build.0 = Release|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -260,5 +272,7 @@ Global
{54F237D5-A70A-4752-9656-0C70B1A7B047} = {B9894058-278A-46B5-B6ED-AD613FCC03B3}
{FB05AC90-89BA-4F2F-A924-F37875FB547C} = {B9894058-278A-46B5-B6ED-AD613FCC03B3}
{E4D9629C-F168-4224-3F51-A5E482FFBC42} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
EndGlobal

8
samples/TestApplication/Program.cs

@ -138,6 +138,7 @@ namespace TestApplication
container.Classes.Add(":container");
DevTools.Attach(window);
window.Show();
Application.Current.Run(window);
}
@ -340,8 +341,11 @@ namespace TestApplication
Foreground = SolidColorBrush.Parse("#727272"),
Margin = new Thickness(0, 0, 0, 10)
},
new TextBox { Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", Width = 200 },
new TextBox { AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, Width = 200, Height = 150, Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." },
new TextBox { Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", Width = 200},
new TextBox { Width = 200, Watermark="Watermark"},
new TextBox { Width = 200, Watermark="Floating Watermark", UseFloatingWatermark = true },
new TextBox { AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, Width = 200, Height = 150, Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." },
new TextBlock
{
Margin = new Thickness(0, 40, 0, 0),

1
samples/XamlTestApplication/Views/MainWindow.paml

@ -3,7 +3,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:XamlTestApplication.ViewModels;assembly=XamlTestApplication"
Title="Perspex Test Application" Height="350" Width="525" SizeToContent="WidthAndHeight" >
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
<TabControl Grid.Row="1" Grid.ColumnSpan="2" Padding="5">
<TabControl.Transition>

2
src/Gtk/Perspex.Cairo/Media/DrawingContext.cs

@ -160,7 +160,7 @@ namespace Perspex.Cairo.Media
/// </summary>
/// <param name="pen">The pen.</param>
/// <param name="rect">The rectangle bounds.</param>
public void DrawRectange(Pen pen, Rect rect, float cornerRadius)
public void DrawRectangle(Pen pen, Rect rect, float cornerRadius)
{
using (var p = SetPen(pen, rect.Size))
{

4
src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs

@ -24,8 +24,8 @@ namespace Perspex.Cairo.Media
TextAlignment textAlignment,
FontWeight fontWeight)
{
Contract.Requires<NullReferenceException>(context != null);
Contract.Requires<NullReferenceException> (text != null);
Contract.Requires<ArgumentNullException>(context != null);
Contract.Requires<ArgumentNullException> (text != null);
Layout = new Pango.Layout(context);
_text = text;
Layout.SetText(text);

12
src/Gtk/Perspex.Gtk/WindowImpl.cs

@ -16,7 +16,7 @@ namespace Perspex.Gtk
public class WindowImpl : Gtk.Window, IWindowImpl
{
private TopLevel _owner;
private IInputRoot _inputRoot;
private IPlatformHandle _windowHandle;
@ -105,9 +105,9 @@ namespace Perspex.Gtk
return new Point(point.X + x, point.Y + y);
}
public void SetOwner(TopLevel owner)
public void SetInputRoot(IInputRoot inputRoot)
{
_owner = owner;
_inputRoot = inputRoot;
}
public void SetTitle(string title)
@ -158,7 +158,7 @@ namespace Perspex.Gtk
var e = new RawMouseEventArgs(
GtkMouseDevice.Instance,
evnt.Time,
_owner,
_inputRoot,
evnt.Button == 0
? RawMouseEventType.LeftButtonDown
: evnt.Button == 1 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown,
@ -172,7 +172,7 @@ namespace Perspex.Gtk
var e = new RawMouseEventArgs(
GtkMouseDevice.Instance,
evnt.Time,
_owner,
_inputRoot,
evnt.Button == 0
? RawMouseEventType.LeftButtonUp
: evnt.Button == 1 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp,
@ -241,7 +241,7 @@ namespace Perspex.Gtk
var e = new RawMouseEventArgs(
GtkMouseDevice.Instance,
evnt.Time,
_owner,
_inputRoot,
RawMouseEventType.Move,
position, GetModifierKeys(evnt.State));
Input(e);

32
src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs

@ -12,6 +12,7 @@ using OmniXaml.Builder;
using OmniXaml.TypeConversion;
using OmniXaml.Typing;
using Perspex.Controls;
using Perspex.Input;
using Perspex.Markup.Xaml.Templates;
using Perspex.Markup.Xaml.Converters;
using Perspex.Markup.Xaml.DataBinding;
@ -21,23 +22,37 @@ using Perspex.Media.Imaging;
using Perspex.Metadata;
using Perspex.Platform;
using Perspex.Styling;
using Perspex.Controls.Primitives;
namespace Perspex.Markup.Xaml.Context
{
public class PerspexWiringContext : IWiringContext
public class PerspexWiringContext : WiringContext
{
private readonly WiringContext _context;
private const string PerspexNs = "https://github.com/perspex";
public PerspexWiringContext(ITypeFactory typeFactory)
: this(typeFactory, new TypeFeatureProvider(GetContentPropertyProvider(), GetConverterProvider()))
{
var featureProvider = new TypeFeatureProvider(GetContentPropertyProvider(), GetConverterProvider());
}
public PerspexWiringContext(ITypeFactory typeFactory, TypeFeatureProvider featureProvider)
: base(CreateTypeContext(typeFactory, featureProvider), featureProvider)
{
}
private static ITypeContext CreateTypeContext(ITypeFactory typeFactory, TypeFeatureProvider featureProvider)
{
var xamlNamespaceRegistry = CreateXamlNamespaceRegistry();
var perspexPropertyBinder = new PerspexPropertyBinder(featureProvider.ConverterProvider);
var xamlTypeRepository = new PerspexTypeRepository(xamlNamespaceRegistry, typeFactory, featureProvider, perspexPropertyBinder);
var typeContext = new TypeContext(xamlTypeRepository, xamlNamespaceRegistry, typeFactory);
_context = new WiringContext(typeContext, featureProvider);
var typeRepository = new PerspexTypeRepository(xamlNamespaceRegistry, typeFactory, featureProvider, perspexPropertyBinder);
typeRepository.RegisterMetadata(new Metadata<Setter>().WithMemberDependency(x => x.Value, x => x.Property));
typeRepository.RegisterMetadata(
new Metadata<SelectingItemsControl>()
.WithMemberDependency(x => x.SelectedIndex, x => x.Items)
.WithMemberDependency(x => x.SelectedItem, x => x.Items));
return new TypeContext(typeRepository, xamlNamespaceRegistry, typeFactory);
}
private static XamlNamespaceRegistry CreateXamlNamespaceRegistry()
@ -88,6 +103,7 @@ namespace Perspex.Markup.Xaml.Context
new TypeConverterRegistration(typeof(Thickness), new ThicknessTypeConverter()),
new TypeConverterRegistration(typeof(Selector), new SelectorTypeConverter()),
new TypeConverterRegistration(typeof(TimeSpan), new TimeSpanTypeConverter()),
new TypeConverterRegistration(typeof(KeyGesture), new KeyGestureConverter())
};
typeConverterProvider.AddAll(converters);
@ -114,9 +130,5 @@ namespace Perspex.Markup.Xaml.Context
return contentPropertyProvider;
}
public ITypeContext TypeContext => _context.TypeContext;
public ITypeFeatureProvider FeatureProvider => _context.FeatureProvider;
}
}

30
src/Markup/Perspex.Markup.Xaml/Converters/KeyGestureConverter.cs

@ -0,0 +1,30 @@
using System;
using System.Globalization;
using OmniXaml.TypeConversion;
using Perspex.Input;
namespace Perspex.Markup.Xaml.Converters
{
class KeyGestureConverter : ITypeConverter
{
public bool CanConvertFrom(IXamlTypeConverterContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public bool CanConvertTo(IXamlTypeConverterContext context, Type destinationType)
{
return false;
}
public object ConvertFrom(IXamlTypeConverterContext context, CultureInfo culture, object value)
{
return KeyGesture.Parse((string)value);
}
public object ConvertTo(IXamlTypeConverterContext context, CultureInfo culture, object value, Type destinationType)
{
throw new NotImplementedException();
}
}
}

1
src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj

@ -39,6 +39,7 @@
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Converters\ColorTypeConverter.cs" />
<Compile Include="Converters\KeyGestureConverter.cs" />
<Compile Include="Converters\RelativePointTypeConverter.cs" />
<Compile Include="Converters\PointTypeConverter.cs" />
<Compile Include="Converters\ClassesTypeConverter.cs" />

102
src/Markup/Perspex.Markup/Binding/ExpressionNode.cs

@ -0,0 +1,102 @@
// 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.Reactive.Subjects;
namespace Perspex.Markup.Binding
{
internal abstract class ExpressionNode : IObservable<ExpressionValue>
{
private object _target;
private Subject<ExpressionValue> _subject;
private ExpressionValue _value = ExpressionValue.None;
public ExpressionNode Next { get; set; }
public object Target
{
get { return _target; }
set
{
if (_target != null)
{
Unsubscribe(_target);
}
_target = value;
if (_target != null)
{
SubscribeAndUpdate(_target);
}
else
{
CurrentValue = ExpressionValue.None;
}
if (Next != null)
{
Next.Target = CurrentValue.Value;
}
}
}
public ExpressionValue CurrentValue
{
get
{
return _value;
}
set
{
_value = value;
if (Next != null)
{
Next.Target = value.Value;
}
if (_subject != null)
{
_subject.OnNext(value);
}
}
}
public virtual bool SetValue(object value)
{
return Next?.SetValue(value) ?? false;
}
public virtual IDisposable Subscribe(IObserver<ExpressionValue> observer)
{
if (Next != null)
{
return Next.Subscribe(observer);
}
else
{
if (_subject == null)
{
_subject = new Subject<ExpressionValue>();
}
observer.OnNext(CurrentValue);
return _subject.Subscribe(observer);
}
}
protected virtual void SubscribeAndUpdate(object target)
{
CurrentValue = new ExpressionValue(target);
}
protected virtual void Unsubscribe(object target)
{
}
}
}

29
src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs

@ -0,0 +1,29 @@
// 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 Perspex.Markup.Binding.Parsers;
namespace Perspex.Markup.Binding
{
internal static class ExpressionNodeBuilder
{
public static ExpressionNode Build(string expression)
{
if (string.IsNullOrWhiteSpace(expression))
{
throw new ArgumentException("'expression' may not be empty.");
}
var reader = new Reader(expression);
var node = ExpressionParser.Parse(reader);
if (!reader.End)
{
throw new ExpressionParseException(reader, "Expected end of expression.");
}
return node;
}
}
}

88
src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs

@ -0,0 +1,88 @@
// 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.Reactive;
using System.Reactive.Disposables;
namespace Perspex.Markup.Binding
{
/// <summary>
/// Observes the value of an expression on a root object.
/// </summary>
public class ExpressionObserver : ObservableBase<ExpressionValue>
{
private int _count;
private ExpressionNode _node;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
public ExpressionObserver(object root, string expression)
{
Root = root;
_node = ExpressionNodeBuilder.Build(expression);
}
/// <summary>
/// Attempts to set the value of a property expression.
/// </summary>
/// <param name="value">The value to set.</param>
/// <returns>
/// True if the value could be set; false if the expression does not evaluate to a
/// property.
/// </returns>
public bool SetValue(object value)
{
IncrementCount();
try
{
return _node.SetValue(value);
}
finally
{
DecrementCount();
}
}
/// <summary>
/// Gets the root object that the expression is being observed on.
/// </summary>
public object Root { get; }
/// <inheritdoc/>
protected override IDisposable SubscribeCore(IObserver<ExpressionValue> observer)
{
IncrementCount();
var subscription = _node.Subscribe(observer);
return Disposable.Create(() =>
{
DecrementCount();
subscription.Dispose();
});
}
private void IncrementCount()
{
if (_count++ == 0)
{
_node.Target = Root;
}
}
private void DecrementCount()
{
if (--_count == 0)
{
_node.Target = null;
}
}
}
}

24
src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs

@ -0,0 +1,24 @@
// 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 Perspex.Markup.Binding.Parsers;
namespace Perspex.Markup.Binding
{
public class ExpressionParseException : Exception
{
internal ExpressionParseException(int column, string message)
: base(message)
{
Column = column;
}
internal ExpressionParseException(Reader r, string message)
: this(r.Position, message)
{
}
public int Column { get; }
}
}

38
src/Markup/Perspex.Markup/Binding/ExpressionValue.cs

@ -0,0 +1,38 @@
// 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;
namespace Perspex.Markup.Binding
{
/// <summary>
/// Holds the value for an <see cref="ExpressionObserver"/>.
/// </summary>
public struct ExpressionValue
{
/// <summary>
/// An <see cref="ExpressionValue"/> that has no value.
/// </summary>
public static readonly ExpressionValue None = new ExpressionValue();
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionValue"/> struct.
/// </summary>
/// <param name="value"></param>
public ExpressionValue(object value)
{
HasValue = true;
Value = value;
}
/// <summary>
/// Gets a value indicating whether the evaluated expression resulted in a value.
/// </summary>
public bool HasValue { get; }
/// <summary>
/// Gets a the result of the expression.
/// </summary>
public object Value { get; }
}
}

106
src/Markup/Perspex.Markup/Binding/IndexerNode.cs

@ -0,0 +1,106 @@
// 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.Collections.Specialized;
using System.Linq;
using System.Reflection;
namespace Perspex.Markup.Binding
{
internal class IndexerNode : ExpressionNode
{
private int[] _intArgs;
public IndexerNode(IList<object> arguments)
{
Arguments = arguments;
var intArgs = Arguments.OfType<int>().ToArray();
if (intArgs.Length == arguments.Count)
{
_intArgs = intArgs;
}
}
public IList<object> Arguments { get; }
protected override void SubscribeAndUpdate(object target)
{
CurrentValue = GetValue(target);
var incc = target as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += CollectionChanged;
}
}
protected override void Unsubscribe(object target)
{
var incc = target as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged -= CollectionChanged;
}
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
bool update = false;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
update = _intArgs[0] >= e.NewStartingIndex;
break;
case NotifyCollectionChangedAction.Remove:
update = _intArgs[0] >= e.OldStartingIndex;
break;
case NotifyCollectionChangedAction.Replace:
update = _intArgs[0] >= e.NewStartingIndex &&
_intArgs[0] < e.NewStartingIndex + e.NewItems.Count;
break;
case NotifyCollectionChangedAction.Move:
update = (_intArgs[0] >= e.NewStartingIndex &&
_intArgs[0] < e.NewStartingIndex + e.NewItems.Count) ||
(_intArgs[0] >= e.OldStartingIndex &&
_intArgs[0] < e.OldStartingIndex + e.OldItems.Count);
break;
case NotifyCollectionChangedAction.Reset:
update = true;
break;
}
if (update)
{
CurrentValue = GetValue(sender);
}
}
private ExpressionValue GetValue(object target)
{
var typeInfo = target.GetType().GetTypeInfo();
var list = target as IList;
if (typeInfo.IsArray && _intArgs != null)
{
return new ExpressionValue(((Array)target).GetValue(_intArgs));
}
else if (target is IList && _intArgs?.Length == 1)
{
if (_intArgs[0] < list.Count)
{
return new ExpressionValue(list[_intArgs[0]]);
}
}
return ExpressionValue.None;
}
}
}

40
src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs

@ -0,0 +1,40 @@
// 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.Globalization;
using System.Reactive.Linq;
namespace Perspex.Markup.Binding
{
internal class LogicalNotNode : ExpressionNode
{
public override bool SetValue(object value)
{
throw new NotSupportedException("Cannot set a negated binding.");
}
public override IDisposable Subscribe(IObserver<ExpressionValue> observer)
{
return Next.Select(x => Negate(x)).Subscribe(observer);
}
private ExpressionValue Negate(ExpressionValue v)
{
if (v.HasValue)
{
try
{
var boolean = Convert.ToBoolean(v.Value, CultureInfo.InvariantCulture);
return new ExpressionValue(!boolean);
}
catch
{
// TODO: Maybe should log something here.
}
}
return ExpressionValue.None;
}
}
}

67
src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs

@ -0,0 +1,67 @@
// 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;
namespace Perspex.Markup.Binding.Parsers
{
internal static class ArgumentListParser
{
public static IList<object> Parse(Reader r, char open, char close)
{
if (r.Peek == open)
{
var result = new List<object>();
r.Take();
while (!r.End)
{
var literal = LiteralParser.Parse(r);
if (literal != null)
{
result.Add(literal);
}
else
{
throw new ExpressionParseException(r, "Expected integer.");
}
r.SkipWhitespace();
if (r.End)
{
throw new ExpressionParseException(r, "Expected ','.");
}
else if (r.TakeIf(close))
{
return result;
}
else
{
if (r.Take() != ',')
{
throw new ExpressionParseException(r, "Expected ','.");
}
r.SkipWhitespace();
}
}
if (!r.End)
{
r.Take();
return result;
}
else
{
throw new ExpressionParseException(r, "Expected ']'.");
}
}
return null;
}
}
}

125
src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs

@ -0,0 +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;
namespace Perspex.Markup.Binding.Parsers
{
internal static class ExpressionParser
{
public static ExpressionNode Parse(Reader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
while (!r.End && state != State.End)
{
switch (state)
{
case State.Start:
state = ParseStart(r, nodes);
break;
case State.AfterMember:
state = ParseAfterMember(r, nodes);
break;
case State.BeforeMember:
state = ParseBeforeMember(r, nodes);
break;
}
}
if (state == State.BeforeMember)
{
throw new ExpressionParseException(r, "Unexpected end of expression.");
}
for (int n = 0; n < nodes.Count - 1; ++n)
{
nodes[n].Next = nodes[n + 1];
}
return nodes.FirstOrDefault();
}
private static State ParseStart(Reader r, IList<ExpressionNode> nodes)
{
if (ParseNot(r))
{
nodes.Add(new LogicalNotNode());
return State.Start;
}
else
{
var identifier = IdentifierParser.Parse(r);
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier));
return State.AfterMember;
}
}
return State.End;
}
private static State ParseAfterMember(Reader r, IList<ExpressionNode> nodes)
{
if (ParseMemberAccessor(r))
{
return State.BeforeMember;
}
else
{
var args = ArgumentListParser.Parse(r, '[', ']');
if (args != null)
{
if (args.Count == 0)
{
throw new ExpressionParseException(r, "Indexer may not be empty.");
}
nodes.Add(new IndexerNode(args));
return State.AfterMember;
}
}
return State.End;
}
private static State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes)
{
var identifier = IdentifierParser.Parse(r);
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier));
return State.AfterMember;
}
return State.End;
}
private static bool ParseNot(Reader r)
{
return !r.End && r.TakeIf('!');
}
private static bool ParseMemberAccessor(Reader r)
{
return !r.End && r.TakeIf('.');
}
private enum State
{
Start,
AfterMember,
BeforeMember,
End,
}
}
}

51
src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs

@ -0,0 +1,51 @@
// 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.Globalization;
using System.Text;
namespace Perspex.Markup.Binding.Parsers
{
internal static class IdentifierParser
{
public static string Parse(Reader r)
{
if (IsValidIdentifierStart(r.Peek))
{
var result = new StringBuilder();
while (!r.End && IsValidIdentifierChar(r.Peek))
{
result.Append(r.Take());
}
return result.ToString();
}
else
{
return null;
}
}
private static bool IsValidIdentifierStart(char c)
{
return char.IsLetter(c) || c == '_';
}
private static bool IsValidIdentifierChar(char c)
{
if (IsValidIdentifierStart(c))
{
return true;
}
else
{
var cat = CharUnicodeInfo.GetUnicodeCategory(c);
return cat == UnicodeCategory.NonSpacingMark ||
cat == UnicodeCategory.SpacingCombiningMark ||
cat == UnicodeCategory.ConnectorPunctuation ||
cat == UnicodeCategory.Format;
}
}
}
}

34
src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs

@ -0,0 +1,34 @@
// 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.Text;
namespace Perspex.Markup.Binding.Parsers
{
internal static class LiteralParser
{
public static object Parse(Reader r)
{
if (char.IsDigit(r.Peek))
{
StringBuilder result = new StringBuilder();
while (!r.End)
{
if (char.IsDigit(r.Peek))
{
result.Append(r.Take());
}
else
{
break;
}
}
return int.Parse(result.ToString());
}
return null;
}
}
}

44
src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs

@ -0,0 +1,44 @@
// 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;
namespace Perspex.Markup.Binding.Parsers
{
internal class Reader
{
private string _s;
private int _i;
public Reader(string s)
{
_s = s;
}
public bool End => _i == _s.Length;
public char Peek => _s[_i];
public int Position => _i;
public char Take() => _s[_i++];
public void SkipWhitespace()
{
while (!End && char.IsWhiteSpace(Peek))
{
Take();
}
}
public bool TakeIf(char c)
{
if (Peek == c)
{
Take();
return true;
}
else
{
return false;
}
}
}
}

140
src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs

@ -0,0 +1,140 @@
// 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.ComponentModel;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Perspex.Markup.Binding
{
internal class PropertyAccessorNode : ExpressionNode
{
private PropertyInfo _propertyInfo;
private IDisposable _subscription;
public PropertyAccessorNode(string propertyName)
{
PropertyName = propertyName;
}
public string PropertyName { get; }
public override bool SetValue(object value)
{
if (Next != null)
{
return Next.SetValue(value);
}
else
{
if (_propertyInfo != null)
{
_propertyInfo.SetValue(Target, value);
return true;
}
return false;
}
}
protected override void SubscribeAndUpdate(object target)
{
bool set = false;
if (target != null)
{
_propertyInfo = target.GetType().GetTypeInfo().GetDeclaredProperty(PropertyName);
if (_propertyInfo != null)
{
ReadValue(target);
set = true;
var inpc = target as INotifyPropertyChanged;
if (inpc != null)
{
inpc.PropertyChanged += PropertyChanged;
}
}
}
else
{
_propertyInfo = null;
}
if (!set)
{
CurrentValue = ExpressionValue.None;
}
}
protected override void Unsubscribe(object target)
{
var inpc = target as INotifyPropertyChanged;
if (inpc != null)
{
inpc.PropertyChanged -= PropertyChanged;
}
}
private void ReadValue(object target)
{
var value = _propertyInfo.GetValue(target);
var observable = value as IObservable<object>;
var task = value as Task;
bool set = false;
if (observable != null)
{
CurrentValue = ExpressionValue.None;
set = true;
_subscription = observable
.ObserveOn(SynchronizationContext.Current)
.Subscribe(x => CurrentValue = new ExpressionValue(x));
}
else if (task != null)
{
var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result");
if (resultProperty != null)
{
if (task.Status == TaskStatus.RanToCompletion)
{
CurrentValue = new ExpressionValue(resultProperty.GetValue(task));
set = true;
}
else
{
task.ContinueWith(
x => CurrentValue = new ExpressionValue(resultProperty.GetValue(task)),
TaskScheduler.FromCurrentSynchronizationContext())
.ConfigureAwait(false);
}
}
}
else
{
CurrentValue = new ExpressionValue(value);
set = true;
}
if (!set)
{
CurrentValue = ExpressionValue.None;
}
}
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == PropertyName)
{
ReadValue(sender);
}
}
}
}

81
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<MinimumVisualStudioVersion>11.0</MinimumVisualStudioVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6417E941-21BC-467B-A771-0DE389353CE6}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Perspex.Markup</RootNamespace>
<AssemblyName>Perspex.Markup</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Compile Include="Binding\ExpressionNodeBuilder.cs" />
<Compile Include="Binding\ExpressionParseException.cs" />
<Compile Include="Binding\ExpressionValue.cs" />
<Compile Include="Binding\LogicalNotNode.cs" />
<Compile Include="Binding\IndexerNode.cs" />
<Compile Include="Binding\Parsers\ArgumentListParser.cs" />
<Compile Include="Binding\Parsers\LiteralParser.cs" />
<Compile Include="Binding\Parsers\IdentifierParser.cs" />
<Compile Include="Binding\Parsers\ExpressionParser.cs" />
<Compile Include="Binding\Parsers\Reader.cs" />
<Compile Include="Binding\PropertyAccessorNode.cs" />
<Compile Include="Binding\ExpressionNode.cs" />
<Compile Include="Binding\ExpressionObserver.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

32
src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs

@ -0,0 +1,32 @@
using System.Resources;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Perspex.Markup")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Perspex.Markup")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: InternalsVisibleTo("Perspex.Markup.UnitTests")]

8
src/Markup/Perspex.Markup/packages.config

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-Linq" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-Main" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable45-net45+win8" />
</packages>

3
src/Perspex.Application/Application.cs

@ -119,6 +119,9 @@ namespace Perspex
private set;
}
/// <summary>
/// Gets the application clipboard.
/// </summary>
public IClipboard Clipboard => _clipboard.Value;
/// <summary>

47
src/Perspex.Base/BindingPriority.cs

@ -0,0 +1,47 @@
// 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.
namespace Perspex
{
/// <summary>
/// The priority of a binding.
/// </summary>
public enum BindingPriority
{
/// <summary>
/// A value that comes from an animation.
/// </summary>
Animation = -1,
/// <summary>
/// A local value.
/// </summary>
LocalValue = 0,
/// <summary>
/// A triggered style binding.
/// </summary>
/// <remarks>
/// A style trigger is a selector such as .class which overrides a
/// <see cref="TemplatedParent"/> binding. In this way, a basic control can have
/// for example a Background from the templated parent which changes when the
/// control has the :pointerover class.
/// </remarks>
StyleTrigger,
/// <summary>
/// A binding to a property on the templated parent.
/// </summary>
TemplatedParent,
/// <summary>
/// A style binding.
/// </summary>
Style,
/// <summary>
/// The binding is uninitialized.
/// </summary>
Unset = int.MaxValue,
}
}

30
src/Perspex.Base/Metadata/XmlnsDefinitionAttribute.cs

@ -1,21 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 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;
namespace Perspex.Metadata
{
/// <summary>
/// Maps an XML namespace to a CLR namespace for use in XAML.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public class XmlnsDefinitionAttribute : Attribute
{
public string XmlNamespace { get; set; }
public string ClrNamespace { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="XmlnsDefinitionAttribute"/> class.
/// </summary>
/// <param name="xmlNamespace">The URL of the XML namespace.</param>
/// <param name="clrNamespace">The CLR namespace.</param>
public XmlnsDefinitionAttribute(string xmlNamespace, string clrNamespace)
{
XmlNamespace = xmlNamespace;
ClrNamespace = clrNamespace;
}
/// <summary>
/// Gets or sets the URL of the XML namespace.
/// </summary>
public string XmlNamespace { get; set; }
/// <summary>
/// Gets or sets the CLR namespace.
/// </summary>
public string ClrNamespace { get; set; }
}
}

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

@ -51,6 +51,7 @@
<Compile Include="PerspexObjectExtensions.cs" />
<Compile Include="PerspexProperty`1.cs" />
<Compile Include="Platform\IPclPlatformWrapper.cs" />
<Compile Include="BindingPriority.cs" />
<Compile Include="PriorityBindingEntry.cs" />
<Compile Include="Collections\IPerspexList.cs" />
<Compile Include="Collections\PerspexList.cs" />

434
src/Perspex.Base/PerspexObject.cs

@ -15,48 +15,6 @@ using Serilog.Core.Enrichers;
namespace Perspex
{
/// <summary>
/// The priority of a binding.
/// </summary>
public enum BindingPriority
{
/// <summary>
/// A value that comes from an animation.
/// </summary>
Animation = -1,
/// <summary>
/// A local value.
/// </summary>
LocalValue = 0,
/// <summary>
/// A triggered style binding.
/// </summary>
/// <remarks>
/// A style trigger is a selector such as .class which overrides a
/// <see cref="TemplatedParent"/> binding. In this way, a basic control can have
/// for example a Background from the templated parent which changes when the
/// control has the :pointerover class.
/// </remarks>
StyleTrigger,
/// <summary>
/// A binding to a property on the templated parent.
/// </summary>
TemplatedParent,
/// <summary>
/// A style binding.
/// </summary>
Style,
/// <summary>
/// The binding is uninitialized.
/// </summary>
Unset = int.MaxValue,
}
/// <summary>
/// An object with <see cref="PerspexProperty"/> support.
/// </summary>
@ -112,11 +70,15 @@ namespace Perspex
foreach (var property in GetRegisteredProperties())
{
object value = property.IsDirect ?
property.Getter(this) :
property.GetDefaultValue(GetType());
var e = new PerspexPropertyChangedEventArgs(
this,
property,
PerspexProperty.UnsetValue,
property.GetDefaultValue(GetType()),
value,
BindingPriority.Unset);
property.NotifyInitialized(e);
@ -222,7 +184,7 @@ namespace Perspex
binding.Mode;
var sourceBinding = value as BindingDescriptor;
if (sourceBinding == null && mode != BindingMode.OneWay)
if (sourceBinding == null && mode > BindingMode.OneWay)
{
throw new InvalidOperationException("Can only bind OneWay to plain IObservable.");
}
@ -253,7 +215,7 @@ namespace Perspex
/// <returns>A collection of <see cref="PerspexProperty"/> definitions.</returns>
public static IEnumerable<PerspexProperty> GetRegisteredProperties(Type type)
{
Contract.Requires<NullReferenceException>(type != null);
Contract.Requires<ArgumentNullException>(type != null);
TypeInfo i = type.GetTypeInfo();
@ -301,8 +263,8 @@ namespace Perspex
/// </remarks>
public static void Register(Type type, PerspexProperty property)
{
Contract.Requires<NullReferenceException>(type != null);
Contract.Requires<NullReferenceException>(property != null);
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(property != null);
List<PerspexProperty> list;
@ -338,7 +300,7 @@ namespace Perspex
/// <param name="property">The property.</param>
public void ClearValue(PerspexProperty property)
{
Contract.Requires<NullReferenceException>(property != null);
Contract.Requires<ArgumentNullException>(property != null);
SetValue(property, PerspexProperty.UnsetValue);
}
@ -350,7 +312,7 @@ namespace Perspex
/// <returns>An observable.</returns>
public IObservable<object> GetObservable(PerspexProperty property)
{
Contract.Requires<NullReferenceException>(property != null);
Contract.Requires<ArgumentNullException>(property != null);
return new PerspexObservable<object>(
observer =>
@ -383,7 +345,7 @@ namespace Perspex
/// <returns>An observable.</returns>
public IObservable<T> GetObservable<T>(PerspexProperty<T> property)
{
Contract.Requires<NullReferenceException>(property != null);
Contract.Requires<ArgumentNullException>(property != null);
return GetObservable((PerspexProperty)property).Cast<T>();
}
@ -425,27 +387,34 @@ namespace Perspex
/// <returns>The value.</returns>
public object GetValue(PerspexProperty property)
{
Contract.Requires<NullReferenceException>(property != null);
Contract.Requires<ArgumentNullException>(property != null);
object result;
PriorityValue value;
if (_values.TryGetValue(property, out value))
if (property.IsDirect)
{
result = value.Value;
return GetRegistered(property).Getter(this);
}
else
{
result = PerspexProperty.UnsetValue;
}
object result = PerspexProperty.UnsetValue;
PriorityValue value;
if (result == PerspexProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
if (!IsRegistered(property))
{
ThrowNotRegistered(property);
}
return result;
if (_values.TryGetValue(property, out value))
{
result = value.Value;
}
if (result == PerspexProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
return result;
}
}
/// <summary>
@ -456,9 +425,16 @@ namespace Perspex
/// <returns>The value.</returns>
public T GetValue<T>(PerspexProperty<T> property)
{
Contract.Requires<NullReferenceException>(property != null);
Contract.Requires<ArgumentNullException>(property != null);
return (T)GetValue((PerspexProperty)property);
if (property.IsDirect)
{
return ((PerspexProperty<T>)GetRegistered(property)).Getter(this);
}
else
{
return (T)GetValue((PerspexProperty)property);
}
}
/// <summary>
@ -479,7 +455,7 @@ namespace Perspex
/// <returns>True if the property is set, otherwise false.</returns>
public bool IsSet(PerspexProperty property)
{
Contract.Requires<NullReferenceException>(property != null);
Contract.Requires<ArgumentNullException>(property != null);
return _values.ContainsKey(property);
}
@ -491,24 +467,7 @@ namespace Perspex
/// <returns>True if the property is registered, otherwise false.</returns>
public bool IsRegistered(PerspexProperty property)
{
Type type = GetType();
while (type != null)
{
List<PerspexProperty> list;
if (s_registered.TryGetValue(type, out list))
{
if (list.Contains(property))
{
return true;
}
}
type = type.GetTypeInfo().BaseType;
}
return false;
return FindRegistered(property) != null;
}
/// <summary>
@ -522,37 +481,50 @@ namespace Perspex
object value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<NullReferenceException>(property != null);
PriorityValue v;
var originalValue = value;
Contract.Requires<ArgumentNullException>(property != null);
if (!IsRegistered(property))
if (property.IsDirect)
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
GetType()));
}
property = GetRegistered(property);
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",
property.Name,
originalValue,
originalValue?.GetType().FullName ?? "(null)"));
}
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
if (!_values.TryGetValue(property, out v))
property.Setter(this, value);
}
else
{
if (value == PerspexProperty.UnsetValue)
PriorityValue v;
var originalValue = value;
if (!IsRegistered(property))
{
ThrowNotRegistered(property);
}
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{
return;
throw new ArgumentException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",
property.Name,
originalValue,
originalValue?.GetType().FullName ?? "(null)"));
}
v = CreatePriorityValue(property);
_values.Add(property, v);
if (!_values.TryGetValue(property, out v))
{
if (value == PerspexProperty.UnsetValue)
{
return;
}
v = CreatePriorityValue(property);
_values.Add(property, v);
}
v.SetValue(value, (int)priority);
}
_propertyLog.Verbose(
@ -560,7 +532,6 @@ namespace Perspex
property,
value,
priority);
v.SetDirectValue(value, (int)priority);
}
/// <summary>
@ -575,9 +546,23 @@ namespace Perspex
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<NullReferenceException>(property != null);
Contract.Requires<ArgumentNullException>(property != null);
if (property.IsDirect)
{
property = (PerspexProperty<T>)GetRegistered(property);
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
SetValue((PerspexProperty)property, value, priority);
property.Setter(this, value);
}
else
{
SetValue((PerspexProperty)property, value, priority);
}
}
/// <summary>
@ -594,32 +579,48 @@ namespace Perspex
IObservable<object> source,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<NullReferenceException>(property != null);
PriorityValue v;
IDescription description = source as IDescription;
Contract.Requires<ArgumentNullException>(property != null);
if (!IsRegistered(property))
if (property.IsDirect)
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
GetType()));
}
property = GetRegistered(property);
if (!_values.TryGetValue(property, out v))
{
v = CreatePriorityValue(property);
_values.Add(property, v);
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
_propertyLog.Verbose(
"Bound {Property} to {Binding} with priority LocalValue",
property,
source);
return source.Subscribe(x => SetValue(property, x));
}
else
{
PriorityValue v;
IDescription description = source as IDescription;
_propertyLog.Verbose(
"Bound {Property} to {Binding} with priority {Priority}",
property,
source,
priority);
if (!IsRegistered(property))
{
ThrowNotRegistered(property);
}
if (!_values.TryGetValue(property, out v))
{
v = CreatePriorityValue(property);
_values.Add(property, v);
}
return v.Add(source, (int)priority);
_propertyLog.Verbose(
"Bound {Property} to {Binding} with priority {Priority}",
property,
source,
priority);
return v.Add(source, (int)priority);
}
}
/// <summary>
@ -637,9 +638,23 @@ namespace Perspex
IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<NullReferenceException>(property != null);
Contract.Requires<ArgumentNullException>(property != null);
return Bind((PerspexProperty)property, source.Select(x => (object)x), priority);
if (property.IsDirect)
{
property = (PerspexProperty<T>)GetRegistered(property);
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
return source.Subscribe(x => SetValue(property, x));
}
else
{
return Bind((PerspexProperty)property, source.Select(x => (object)x), priority);
}
}
/// <summary>
@ -713,6 +728,69 @@ namespace Perspex
{
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event.
/// </summary>
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected void RaisePropertyChanged(
PerspexProperty property,
object oldValue,
object newValue,
BindingPriority priority)
{
Contract.Requires<ArgumentNullException>(property != null);
PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs(
this,
property,
oldValue,
newValue,
priority);
OnPropertyChanged(e);
property.NotifyChanged(e);
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
if (_inpcChanged != null)
{
PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
_inpcChanged(this, e2);
}
}
/// <summary>
/// Sets the backing field for a direct perspex property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="value">The value.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
protected bool SetAndRaise<T>(PerspexProperty<T> property, ref T field, T value)
{
if (!object.Equals(field, value))
{
var old = field;
field = value;
RaisePropertyChanged(property, old, value, BindingPriority.LocalValue);
return true;
}
else
{
return false;
}
}
/// <summary>
/// Creates a <see cref="PriorityValue"/> for a <see cref="PerspexProperty"/>.
/// </summary>
@ -772,6 +850,59 @@ 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.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The registered property.</returns>
public PerspexProperty GetRegistered(PerspexProperty property)
{
var result = FindRegistered(property);
if (result == null)
{
ThrowNotRegistered(property);
}
return result;
}
/// <summary>
/// Called when a property is changed on the current <see cref="InheritanceParent"/>.
/// </summary>
@ -801,40 +932,13 @@ namespace Perspex
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event.
/// Throws an exception indicating that the specified property is not registered on this
/// object.
/// </summary>
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
private void RaisePropertyChanged(
PerspexProperty property,
object oldValue,
object newValue,
BindingPriority priority)
/// <param name="p">The property</param>
private void ThrowNotRegistered(PerspexProperty p)
{
Contract.Requires<NullReferenceException>(property != null);
PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs(
this,
property,
oldValue,
newValue,
priority);
OnPropertyChanged(e);
property.NotifyChanged(e);
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
if (_inpcChanged != null)
{
PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
_inpcChanged(this, e2);
}
throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}");
}
}
}

221
src/Perspex.Base/PerspexProperty.cs

@ -15,13 +15,18 @@ namespace Perspex
/// <remarks>
/// This class is analogous to DependencyProperty in WPF.
/// </remarks>
public class PerspexProperty
public class PerspexProperty : IEquatable<PerspexProperty>
{
/// <summary>
/// Represents an unset property value.
/// </summary>
public static readonly object UnsetValue = new Unset();
/// <summary>
/// Gets the next ID that will be allocated to a property.
/// </summary>
private static int s_nextId = 1;
/// <summary>
/// The default values for the property, by type.
/// </summary>
@ -43,6 +48,11 @@ namespace Perspex
private readonly Dictionary<Type, Func<PerspexObject, object, object>> _validation =
new Dictionary<Type, Func<PerspexObject, object, object>>();
/// <summary>
/// Gets the ID of the property.
/// </summary>
private int _id;
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
@ -64,9 +74,9 @@ namespace Perspex
Func<PerspexObject, object, object> validate = null,
bool isAttached = false)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(valueType != null);
Contract.Requires<NullReferenceException>(ownerType != null);
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(valueType != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
if (name.Contains("."))
{
@ -80,6 +90,7 @@ namespace Perspex
Inherits = inherits;
DefaultBindingMode = defaultBindingMode;
IsAttached = isAttached;
_id = s_nextId++;
if (validate != null)
{
@ -87,6 +98,69 @@ namespace Perspex
}
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="valueType">The type of the property's value.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
public PerspexProperty(
string name,
Type valueType,
Type ownerType,
Func<PerspexObject, object> getter,
Action<PerspexObject, object> setter)
{
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(valueType != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<ArgumentNullException>(getter != null);
if (name.Contains("."))
{
throw new ArgumentException("'name' may not contain periods.");
}
Name = name;
PropertyType = valueType;
OwnerType = ownerType;
Getter = getter;
Setter = setter;
IsDirect = true;
_id = s_nextId++;
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="source">The direct property to copy.</param>
/// <param name="getter">A new getter.</param>
/// <param name="setter">A new setter.</param>
protected PerspexProperty(
PerspexProperty source,
Func<PerspexObject, object> getter,
Action<PerspexObject, object> setter)
{
Contract.Requires<ArgumentNullException>(source != null);
Contract.Requires<ArgumentNullException>(getter != null);
if (!source.IsDirect)
{
throw new InvalidOperationException(
"This method can only be called on direct PerspexProperties.");
}
Name = source.Name;
PropertyType = source.PropertyType;
OwnerType = source.OwnerType;
Getter = getter;
Setter = setter;
IsDirect = true;
_id = source._id;
}
/// <summary>
/// Gets the name of the property.
/// </summary>
@ -135,6 +209,11 @@ namespace Perspex
/// </value>
public bool IsAttached { get; }
/// <summary>
/// Gets a value indicating whether this is a direct property.
/// </summary>
public bool IsDirect { get; }
/// <summary>
/// Gets an observable that is fired when this property is initialized on a
/// new <see cref="PerspexObject"/> instance.
@ -191,6 +270,49 @@ namespace Perspex
};
}
/// <summary>
/// Gets the getter function for direct properties.
/// </summary>
internal Func<PerspexObject, object> Getter { get; }
/// <summary>
/// Gets the etter function for direct properties.
/// </summary>
internal Action<PerspexObject, object> Setter { get; }
/// <summary>
/// Tests two <see cref="PerspexProperty"/>s for equality.
/// </summary>
/// <param name="a">The first property.</param>
/// <param name="b">The second property.</param>
/// <returns>True if the properties are equal, otherwise false.</returns>
public static bool operator ==(PerspexProperty a, PerspexProperty b)
{
if (object.ReferenceEquals(a, b))
{
return true;
}
else if (((object)a == null) || ((object)b == null))
{
return false;
}
else
{
return a.Equals(b);
}
}
/// <summary>
/// Tests two <see cref="PerspexProperty"/>s for unequality.
/// </summary>
/// <param name="a">The first property.</param>
/// <param name="b">The second property.</param>
/// <returns>True if the properties are equal, otherwise false.</returns>
public static bool operator !=(PerspexProperty a, PerspexProperty b)
{
return !(a == b);
}
/// <summary>
/// Registers a <see cref="PerspexProperty"/>.
/// </summary>
@ -210,7 +332,7 @@ namespace Perspex
Func<TOwner, TValue, TValue> validate = null)
where TOwner : PerspexObject
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<ArgumentNullException>(name != null);
PerspexProperty<TValue> result = new PerspexProperty<TValue>(
name,
@ -226,6 +348,34 @@ namespace Perspex
return result;
}
/// <summary>
/// Registers a direct <see cref="PerspexProperty"/>.
/// </summary>
/// <typeparam name="TOwner">The type of the class that is registering the property.</typeparam>
/// <typeparam name="TValue">The type of the property's value.</typeparam>
/// <param name="name">The name of the property.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
/// <returns>A <see cref="PerspexProperty{TValue}"/></returns>
public static PerspexProperty<TValue> RegisterDirect<TOwner, TValue>(
string name,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter = null)
where TOwner : PerspexObject
{
Contract.Requires<ArgumentNullException>(name != null);
PerspexProperty<TValue> result = new PerspexProperty<TValue>(
name,
typeof(TOwner),
Cast(getter),
Cast(setter));
PerspexObject.Register(typeof(TOwner), result);
return result;
}
/// <summary>
/// Registers an attached <see cref="PerspexProperty"/>.
/// </summary>
@ -245,7 +395,7 @@ namespace Perspex
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<PerspexObject, TValue, TValue> validate = null)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<ArgumentNullException>(name != null);
PerspexProperty<TValue> result = new PerspexProperty<TValue>(
name,
@ -281,7 +431,7 @@ namespace Perspex
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<PerspexObject, TValue, TValue> validate = null)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<ArgumentNullException>(name != null);
PerspexProperty<TValue> result = new PerspexProperty<TValue>(
name,
@ -297,6 +447,25 @@ namespace Perspex
return result;
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
var p = obj as PerspexProperty;
return p != null ? Equals(p) : false;
}
/// <inheritdoc/>
public bool Equals(PerspexProperty other)
{
return other != null && _id == other._id;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return _id;
}
/// <summary>
/// Returns a binding accessor that can be passed to <see cref="PerspexObject"/>'s []
/// operator to initiate a binding.
@ -320,7 +489,7 @@ namespace Perspex
/// <returns>The default value.</returns>
public object GetDefaultValue(Type type)
{
Contract.Requires<NullReferenceException>(type != null);
Contract.Requires<ArgumentNullException>(type != null);
while (type != null)
{
@ -346,7 +515,7 @@ namespace Perspex
/// </returns>
public Func<PerspexObject, object, object> GetValidationFunc(Type type)
{
Contract.Requires<NullReferenceException>(type != null);
Contract.Requires<ArgumentNullException>(type != null);
while (type != null)
{
@ -390,7 +559,7 @@ namespace Perspex
/// <param name="defaultValue">The default value.</param>
public void OverrideDefaultValue(Type type, object defaultValue)
{
Contract.Requires<NullReferenceException>(type != null);
Contract.Requires<ArgumentNullException>(type != null);
if (!TypeUtilities.TryCast(PropertyType, defaultValue, out defaultValue))
{
@ -416,7 +585,7 @@ namespace Perspex
/// <param name="validation">The validation function.</param>
public void OverrideValidation(Type type, Func<PerspexObject, object, object> validation)
{
Contract.Requires<NullReferenceException>(type != null);
Contract.Requires<ArgumentNullException>(type != null);
if (_validation.ContainsKey(type))
{
@ -453,9 +622,37 @@ namespace Perspex
_changed.OnNext(e);
}
/// <summary>
/// Casts a getter function accepting a typed owner to one accepting a
/// <see cref="PerspexObject"/>.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <typeparam name="TValue">The property value type.</typeparam>
/// <param name="f">The typed function.</param>
/// <returns>The untyped function.</returns>
private static Func<PerspexObject, TValue> Cast<TOwner, TValue>(Func<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? o => f((TOwner)o) : (Func<PerspexObject, TValue >)null;
}
/// <summary>
/// Casts a setter action accepting a typed owner to one accepting a
/// <see cref="PerspexObject"/>.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <typeparam name="TValue">The property value type.</typeparam>
/// <param name="f">The typed action.</param>
/// <returns>The untyped action.</returns>
private static Action<PerspexObject, TValue> Cast<TOwner, TValue>(Action<TOwner, TValue> f)
where TOwner : PerspexObject
{
return f != null ? (o, v) => f((TOwner)o, v) : (Action<PerspexObject, TValue>)null;
}
/// <summary>
/// Casts a validation function accepting a typed owner to one accepting a
/// <see cref="Perspex"/>.
/// <see cref="PerspexObject"/>.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <typeparam name="TValue">The property value type.</typeparam>

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

@ -36,24 +36,87 @@ namespace Perspex
defaultValue,
inherits,
defaultBindingMode,
Convert(validate),
Cast(validate),
isAttached)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(ownerType != null);
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty{TValue}"/> class.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
public PerspexProperty(
string name,
Type ownerType,
Func<PerspexObject, TValue> getter,
Action<PerspexObject, TValue> setter)
: base(name, typeof(TValue), ownerType, CastParamReturn(getter), CastParams(setter))
{
Getter = getter;
Setter = setter;
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="source">The direct property to copy.</param>
/// <param name="getter">A new getter.</param>
/// <param name="setter">A new setter.</param>
private PerspexProperty(
PerspexProperty source,
Func<PerspexObject, TValue> getter,
Action<PerspexObject, TValue> setter)
: base(source, CastParamReturn(getter), CastParams(setter))
{
Getter = getter;
Setter = setter;
}
/// <summary>
/// Gets the getter function for direct properties.
/// </summary>
internal new Func<PerspexObject, TValue> Getter { get; }
/// <summary>
/// Gets the etter function for direct properties.
/// </summary>
internal new Action<PerspexObject, TValue> Setter { get; }
/// <summary>
/// Registers the property on another type.
/// </summary>
/// <typeparam name="TOwner">The type of the additional owner.</typeparam>
/// <returns>The property.</returns>
public PerspexProperty<TValue> AddOwner<TOwner>()
public PerspexProperty<TValue> AddOwner<TOwner>() where TOwner : PerspexObject
{
if (IsDirect)
{
throw new InvalidOperationException(
"You must provide a new getter and setter when calling AddOwner on a direct PerspexProperty.");
}
PerspexObject.Register(typeof(TOwner), this);
return this;
}
/// <summary>
/// Registers the direct property on another type.
/// </summary>
/// <typeparam name="TOwner">The type of the additional owner.</typeparam>
/// <returns>The property.</returns>
public PerspexProperty<TValue> AddOwner<TOwner>(
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter = null)
where TOwner : PerspexObject
{
var result = new PerspexProperty<TValue>(this, CastReturn(getter), CastParam1(setter));
PerspexObject.Register(typeof(TOwner), result);
return result;
}
/// <summary>
/// Gets the default value for the property on the specified type.
/// </summary>
@ -78,11 +141,59 @@ namespace Perspex
}
/// <summary>
/// Converts from a typed validation function to an untyped.
/// Casts a typed getter function to an untyped.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <param name="f">The typed function.</param>
/// <returns>The untyped function.</returns>
private static Func<PerspexObject, object> CastParamReturn<TOwner>(Func<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? o => f((TOwner)o) : (Func<PerspexObject, object>)null;
}
/// <summary>
/// Casts a typed getter function to an untyped.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <param name="f">The typed function.</param>
/// <returns>The untyped function.</returns>
private static Func<PerspexObject, TValue> CastReturn<TOwner>(Func<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? o => f((TOwner)o) : (Func<PerspexObject, TValue>)null;
}
/// <summary>
/// Casts a typed setter function to an untyped.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <param name="f">The typed function.</param>
/// <returns>The untyped function.</returns>
private static Action<PerspexObject, object> CastParams<TOwner>(Action<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? (o, v) => f((TOwner)o, (TValue)v) : (Action<PerspexObject, object>)null;
}
/// <summary>
/// Casts a typed setter function to an untyped.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <param name="f">The typed function.</param>
/// <returns>The untyped function.</returns>
private static Action<PerspexObject, TValue> CastParam1<TOwner>(Action<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? (o, v) => f((TOwner)o, v) : (Action<PerspexObject, TValue>)null;
}
/// <summary>
/// Casts a typed validation function to an untyped.
/// </summary>
/// <param name="f">The typed validation function.</param>
/// <returns>The untyped validation function.</returns>
private static Func<PerspexObject, object, object> Convert(Func<PerspexObject, TValue, TValue> f)
private static Func<PerspexObject, object, object> Cast(Func<PerspexObject, TValue, TValue> f)
{
return f != null ? (o, v) => f(o, (TValue)v) : (Func<PerspexObject, object, object>)null;
}

4
src/Perspex.Base/PriorityValue.cs

@ -105,11 +105,11 @@ namespace Perspex
}
/// <summary>
/// Sets the direct value for a specified priority.
/// Sets the value for a specified priority.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="priority">The priority</param>
public void SetDirectValue(object value, int priority)
public void SetValue(object value, int priority)
{
GetLevel(priority).DirectValue = value;
}

2
src/Perspex.Base/Utilities/TypeUtilities.cs

@ -35,7 +35,7 @@ namespace Perspex.Utilities
/// <returns>True if the cast was sucessful, otherwise false.</returns>
public static bool TryCast(Type to, object value, out object result)
{
Contract.Requires<NullReferenceException>(to != null);
Contract.Requires<ArgumentNullException>(to != null);
if (value == null)
{

2
src/Perspex.Controls/Border.cs

@ -98,7 +98,7 @@ namespace Perspex.Controls
if (borderBrush != null && borderThickness > 0)
{
context.DrawRectange(new Pen(borderBrush, borderThickness), rect, cornerRadius);
context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
}
}

12
src/Perspex.Controls/Button.cs

@ -44,6 +44,9 @@ namespace Perspex.Controls
public static readonly PerspexProperty<ICommand> CommandProperty =
PerspexProperty.Register<Button, ICommand>(nameof(Command));
public static readonly PerspexProperty<KeyGesture> HotKeyProperty =
HotKeyManager.HotKeyProperty.AddOwner<Button>();
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
/// </summary>
@ -100,6 +103,15 @@ namespace Perspex.Controls
set { SetValue(CommandProperty, value); }
}
/// <summary>
/// Gets or sets an <see cref="KeyGesture"/> associated with this control
/// </summary>
public KeyGesture HotKey
{
get { return GetValue(HotKeyProperty); }
set { SetValue(HotKeyProperty, value); }
}
/// <summary>
/// Gets or sets a parameter to be passed to the <see cref="Command"/>.
/// </summary>

26
src/Perspex.Controls/Control.cs

@ -2,14 +2,12 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Linq;
using Perspex.Collections;
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
using Perspex.Input;
using Perspex.Interactivity;
using Perspex.LogicalTree;
using Perspex.Rendering;
using Perspex.Styling;
@ -46,7 +44,7 @@ namespace Perspex.Controls
/// Defines the <see cref="Parent"/> property.
/// </summary>
public static readonly PerspexProperty<IControl> ParentProperty =
PerspexProperty.Register<Control, IControl>(nameof(Parent));
PerspexProperty.RegisterDirect<Control, IControl>(nameof(Parent), o => o.Parent);
/// <summary>
/// Defines the <see cref="Tag"/> property.
@ -66,16 +64,12 @@ namespace Perspex.Controls
public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
private IControl _parent;
private readonly Classes _classes = new Classes();
private DataTemplates _dataTemplates;
private IControl _focusAdorner;
private string _id;
private string _name;
private IPerspexList<ILogical> _logicalChildren;
private Styles _styles;
/// <summary>
@ -179,22 +173,22 @@ namespace Perspex.Controls
{
get
{
return _id;
return _name;
}
set
{
if (_id != null)
if (_name != null)
{
throw new InvalidOperationException("ID already set.");
throw new InvalidOperationException("Name already set.");
}
if (((IVisual)this).VisualParent != null)
{
throw new InvalidOperationException("Cannot set ID : control already added to tree.");
throw new InvalidOperationException("Cannot set Name : control already added to tree.");
}
_id = value;
_name = value;
}
}
@ -227,7 +221,7 @@ namespace Perspex.Controls
/// <summary>
/// Gets the control's logical parent.
/// </summary>
public IControl Parent => GetValue(ParentProperty);
public IControl Parent => _parent;
/// <summary>
/// Gets or sets a user-defined object attached to the control.
@ -297,7 +291,7 @@ namespace Perspex.Controls
throw new InvalidOperationException("The Control already has a parent.");
}
SetValue(ParentProperty, parent);
SetAndRaise(ParentProperty, ref _parent, (IControl)parent);
}
/// <summary>

12
src/Perspex.Controls/DropDown.cs

@ -26,11 +26,15 @@ namespace Perspex.Controls
ContentControl.VerticalContentAlignmentProperty.AddOwner<DropDown>();
public static readonly PerspexProperty<bool> IsDropDownOpenProperty =
PerspexProperty.Register<DropDown, bool>("IsDropDownOpen");
PerspexProperty.RegisterDirect<DropDown, bool>(
nameof(IsDropDownOpen),
o => o.IsDropDownOpen,
(o, v) => o.IsDropDownOpen = v);
public static readonly PerspexProperty<object> SelectionBoxItemProperty =
PerspexProperty.Register<DropDown, object>("SelectionBoxItem");
private bool _isDropDownOpen;
private Popup _popup;
static DropDown()
@ -64,8 +68,8 @@ namespace Perspex.Controls
public bool IsDropDownOpen
{
get { return GetValue(IsDropDownOpenProperty); }
set { SetValue(IsDropDownOpenProperty, value); }
get { return _isDropDownOpen; }
set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); }
}
public object SelectionBoxItem
@ -140,6 +144,8 @@ namespace Perspex.Controls
if (control != null)
{
control.Measure(Size.Infinity);
SelectionBoxItem = new Rectangle
{
Width = control.DesiredSize.Width,

117
src/Perspex.Controls/HotkeyManager.cs

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Perspex.Controls;
using Perspex.Controls.Utils;
using Perspex.Input;
namespace Perspex.Controls
{
public class HotKeyManager
{
public static PerspexProperty<KeyGesture> HotKeyProperty
= PerspexProperty.RegisterAttached<Control, KeyGesture>("HotKey", typeof (HotKeyManager));
class HotkeyCommandWrapper : ICommand
{
public HotkeyCommandWrapper(IControl control)
{
Control = control;
}
public IControl Control;
private ICommand GetCommand() => Control.GetValue(Button.CommandProperty);
public bool CanExecute(object parameter) => GetCommand()?.CanExecute(parameter) ?? false;
public void Execute(object parameter) => GetCommand()?.Execute(parameter);
//Implementation isn't needed in this case
public event EventHandler CanExecuteChanged;
}
class Manager
{
private readonly IControl _control;
private TopLevel _root;
private IDisposable _parentSub;
private IDisposable _hotkeySub;
private KeyGesture _hotkey;
private HotkeyCommandWrapper _wrapper;
private KeyBinding _binding;
public Manager(IControl control)
{
_control = control;
_wrapper = new HotkeyCommandWrapper(_control);
}
public void Init()
{
_hotkeySub = _control.GetObservable(HotKeyProperty).Subscribe(OnHotkeyChanged);
_parentSub = AncestorFinder.Create(_control, typeof (TopLevel)).Subscribe(OnParentChanged);
}
private void OnParentChanged(IControl control)
{
Unregister();
_root = (TopLevel) control;
Register();
}
private void OnHotkeyChanged(KeyGesture hotkey)
{
if (hotkey == null)
//Subscription will be recreated by static property watcher
Stop();
else
{
Unregister();
_hotkey = hotkey;
Register();
}
}
void Unregister()
{
if (_root != null && _binding != null)
_root.KeyBindings.Remove(_binding);
_binding = null;
}
void Register()
{
if (_root != null && _hotkey != null)
{
_binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper};
_root.KeyBindings.Add(_binding);
}
}
void Stop()
{
Unregister();
_parentSub.Dispose();
_hotkeySub.Dispose();
}
}
static HotKeyManager()
{
HotKeyProperty.Changed.Subscribe(args =>
{
var control = args.Sender as IControl;
if (args.OldValue != null|| control == null)
return;
new Manager(control).Init();
});
}
public static void SetHotKey(PerspexObject target, KeyGesture value) => target.SetValue(HotKeyProperty, value);
public static KeyGesture GetHotKey(PerspexObject target) => target.GetValue(HotKeyProperty);
}
}

53
src/Perspex.Controls/ItemsControl.cs

@ -33,7 +33,7 @@ namespace Perspex.Controls
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly PerspexProperty<IEnumerable> ItemsProperty =
PerspexProperty.Register<ItemsControl, IEnumerable>(nameof(Items));
PerspexProperty.RegisterDirect<ItemsControl, IEnumerable>(nameof(Items), o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="ItemsPanel"/> property.
@ -47,6 +47,7 @@ namespace Perspex.Controls
public static readonly PerspexProperty<IMemberSelector> MemberSelectorProperty =
PerspexProperty.Register<ItemsControl, IMemberSelector>(nameof(MemberSelector));
private IEnumerable _items = new PerspexList<object>();
private IItemContainerGenerator _itemContainerGenerator;
/// <summary>
@ -63,7 +64,7 @@ namespace Perspex.Controls
public ItemsControl()
{
Classes.Add(":empty");
Items = new PerspexList<object>();
SubscribeToItems(_items);
}
/// <summary>
@ -87,8 +88,8 @@ namespace Perspex.Controls
/// </summary>
public IEnumerable Items
{
get { return GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
get { return _items; }
set { SetAndRaise(ItemsProperty, ref _items, value); }
}
/// <summary>
@ -155,30 +156,17 @@ namespace Perspex.Controls
/// <param name="e">The event args.</param>
protected virtual void ItemsChanged(PerspexPropertyChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"{this.GetType().Name} set items");
var incc = e.OldValue as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += ItemsCollectionChanged;
incc.CollectionChanged -= ItemsCollectionChanged;
}
var newValue = e.NewValue as IEnumerable;
if (newValue == null || newValue.Count() == 0)
{
Classes.Add(":empty");
}
else
{
Classes.Remove(":empty");
}
incc = newValue as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += ItemsCollectionChanged;
}
SubscribeToItems(newValue);
}
/// <summary>
@ -200,5 +188,28 @@ namespace Perspex.Controls
Classes.Remove(":empty");
}
}
/// <summary>
/// Subscribes to an <see cref="Items"/> collection.
/// </summary>
/// <param name="items"></param>
private void SubscribeToItems(IEnumerable items)
{
if (items == null || items.Count() == 0)
{
Classes.Add(":empty");
}
else
{
Classes.Remove(":empty");
}
var incc = items as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += ItemsCollectionChanged;
}
}
}
}

10
src/Perspex.Controls/Menu.cs

@ -28,7 +28,11 @@ namespace Perspex.Controls
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsOpenProperty =
PerspexProperty.Register<Menu, bool>(nameof(IsOpen));
PerspexProperty.RegisterDirect<Menu, bool>(
nameof(IsOpen),
o => o.IsOpen);
private bool _isOpen;
/// <summary>
/// Tracks event handlers added to the root of the visual tree.
@ -50,8 +54,8 @@ namespace Perspex.Controls
/// </summary>
public bool IsOpen
{
get { return GetValue(IsOpenProperty); }
private set { SetValue(IsOpenProperty, value); }
get { return _isOpen; }
private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
/// <summary>

13
src/Perspex.Controls/MenuItem.cs

@ -28,6 +28,9 @@ namespace Perspex.Controls
public static readonly PerspexProperty<ICommand> CommandProperty =
Button.CommandProperty.AddOwner<MenuItem>();
public static readonly PerspexProperty<KeyGesture> HotKeyProperty =
HotKeyManager.HotKeyProperty.AddOwner<Button>();
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
/// </summary>
@ -130,6 +133,16 @@ namespace Perspex.Controls
set { SetValue(CommandProperty, value); }
}
/// <summary>
/// Gets or sets an <see cref="KeyGesture"/> associated with this control
/// </summary>
public KeyGesture HotKey
{
get { return GetValue(HotKeyProperty); }
set { SetValue(HotKeyProperty, value); }
}
/// <summary>
/// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
/// <see cref="MenuItem"/>.

2
src/Perspex.Controls/Perspex.Controls.csproj

@ -42,6 +42,7 @@
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="DockPanel.cs" />
<Compile Include="HotkeyManager.cs" />
<Compile Include="SystemDialog.cs" />
<Compile Include="Generators\ITreeItemContainerGenerator.cs" />
<Compile Include="Generators\ItemContainers.cs" />
@ -132,6 +133,7 @@
<Compile Include="UserControl.cs" />
<Compile Include="Templates\TemplateExtensions.cs" />
<Compile Include="TextWrapping.cs" />
<Compile Include="Utils\AncestorFinder.cs" />
<Compile Include="Utils\IEnumerableUtils.cs" />
<Compile Include="Utils\StringUtils.cs" />
<Compile Include="TopLevel.cs" />

2
src/Perspex.Controls/Platform/ITopLevelImpl.cs

@ -30,7 +30,7 @@ namespace Perspex.Platform
void Invalidate(Rect rect);
void SetOwner(TopLevel owner);
void SetInputRoot(IInputRoot inputRoot);
Point PointToScreen(Point point);

17
src/Perspex.Controls/Presenters/ContentPresenter.cs

@ -15,7 +15,9 @@ namespace Perspex.Controls.Presenters
/// Defines the <see cref="Child"/> property.
/// </summary>
public static readonly PerspexProperty<IControl> ChildProperty =
PerspexProperty.Register<ContentPresenter, IControl>("Child");
PerspexProperty.RegisterDirect<ContentPresenter, IControl>(
nameof(Child),
o => o.Child);
/// <summary>
/// Defines the <see cref="Content"/> property.
@ -23,6 +25,7 @@ namespace Perspex.Controls.Presenters
public static readonly PerspexProperty<object> ContentProperty =
ContentControl.ContentProperty.AddOwner<ContentPresenter>();
private IControl _child;
private bool _createdChild;
/// <summary>
@ -38,8 +41,8 @@ namespace Perspex.Controls.Presenters
/// </summary>
public IControl Child
{
get { return GetValue(ChildProperty); }
private set { SetValue(ChildProperty, value); }
get { return _child; }
private set { SetAndRaise(ChildProperty, ref _child, value); }
}
/// <summary>
@ -101,8 +104,12 @@ namespace Perspex.Controls.Presenters
var logicalHost = this.FindReparentingHost();
var logicalChildren = logicalHost?.LogicalChildren ?? LogicalChildren;
logicalChildren.Remove(old);
ClearVisualChildren();
if (old != null)
{
logicalChildren.Remove(old);
((ISetLogicalParent)old).SetParent(null);
ClearVisualChildren();
}
Child = result;

19
src/Perspex.Controls/Presenters/DeckPresenter.cs

@ -23,7 +23,7 @@ namespace Perspex.Controls.Presenters
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly PerspexProperty<IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<DeckPresenter>();
ItemsControl.ItemsProperty.AddOwner<DeckPresenter>(o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="ItemsPanel"/> property.
@ -41,7 +41,9 @@ namespace Perspex.Controls.Presenters
/// Defines the <see cref="SelectedIndex"/> property.
/// </summary>
public static readonly PerspexProperty<int> SelectedIndexProperty =
SelectingItemsControl.SelectedIndexProperty.AddOwner<DeckPresenter>();
SelectingItemsControl.SelectedIndexProperty.AddOwner<DeckPresenter>(
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v);
/// <summary>
/// Defines the <see cref="Transition"/> property.
@ -49,8 +51,9 @@ namespace Perspex.Controls.Presenters
public static readonly PerspexProperty<IPageTransition> TransitionProperty =
Deck.TransitionProperty.AddOwner<DeckPresenter>();
private IEnumerable _items;
private int _selectedIndex = -1;
private bool _createdPanel;
private IItemContainerGenerator _generator;
/// <summary>
@ -94,8 +97,8 @@ namespace Perspex.Controls.Presenters
/// </summary>
public IEnumerable Items
{
get { return GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
get { return _items; }
set { SetAndRaise(ItemsProperty, ref _items, value); }
}
/// <summary>
@ -121,8 +124,8 @@ namespace Perspex.Controls.Presenters
/// </summary>
public int SelectedIndex
{
get { return GetValue(SelectedIndexProperty); }
set { SetValue(SelectedIndexProperty, value); }
get { return _selectedIndex; }
set { SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); }
}
/// <summary>
@ -204,7 +207,7 @@ namespace Perspex.Controls.Presenters
}
}
if (Transition != null)
if (Transition != null && (from != null || to != null))
{
await Transition.Start((Visual)from, (Visual)to, fromIndex < toIndex);
}

8
src/Perspex.Controls/Presenters/ItemsPresenter.cs

@ -20,7 +20,7 @@ namespace Perspex.Controls.Presenters
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly PerspexProperty<IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsPresenter>();
ItemsControl.ItemsProperty.AddOwner<ItemsPresenter>(o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="ItemsPanel"/> property.
@ -34,8 +34,8 @@ namespace Perspex.Controls.Presenters
public static readonly PerspexProperty<IMemberSelector> MemberSelectorProperty =
ItemsControl.MemberSelectorProperty.AddOwner<ItemsPresenter>();
private IEnumerable _items;
private bool _createdPanel;
private IItemContainerGenerator _generator;
/// <summary>
@ -89,8 +89,8 @@ namespace Perspex.Controls.Presenters
/// </summary>
public IEnumerable Items
{
get { return GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
get { return _items; }
set { SetAndRaise(ItemsProperty, ref _items, value); }
}
/// <summary>

64
src/Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -31,18 +31,19 @@ namespace Perspex.Controls.Primitives
/// Defines the <see cref="SelectedIndex"/> property.
/// </summary>
public static readonly PerspexProperty<int> SelectedIndexProperty =
PerspexProperty.Register<SelectingItemsControl, int>(
PerspexProperty.RegisterDirect<SelectingItemsControl, int>(
nameof(SelectedIndex),
defaultValue: -1,
validate: ValidateSelectedIndex);
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v);
/// <summary>
/// Defines the <see cref="SelectedItem"/> property.
/// </summary>
public static readonly PerspexProperty<object> SelectedItemProperty =
PerspexProperty.Register<SelectingItemsControl, object>(
PerspexProperty.RegisterDirect<SelectingItemsControl, object>(
nameof(SelectedItem),
validate: ValidateSelectedItem);
o => o.SelectedItem,
(o, v) => o.SelectedItem = v);
/// <summary>
/// Event that should be raised by items that implement <see cref="ISelectable"/> to
@ -52,6 +53,9 @@ namespace Perspex.Controls.Primitives
public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent =
RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>("IsSelectedChanged", RoutingStrategies.Bubble);
private int _selectedIndex = -1;
private object _selectedItem;
/// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
/// </summary>
@ -85,8 +89,16 @@ namespace Perspex.Controls.Primitives
/// </summary>
public int SelectedIndex
{
get { return GetValue(SelectedIndexProperty); }
set { SetValue(SelectedIndexProperty, value); }
get
{
return _selectedIndex;
}
set
{
value = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1;
SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value);
}
}
/// <summary>
@ -94,8 +106,16 @@ namespace Perspex.Controls.Primitives
/// </summary>
public object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
get
{
return _selectedItem;
}
set
{
value = Items?.Cast<object>().Contains(value) == true ? value : null;
SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
}
}
/// <inheritdoc/>
@ -234,30 +254,6 @@ namespace Perspex.Controls.Primitives
}
}
/// <summary>
/// Coerces the <see cref="SelectedIndex"/> property.
/// </summary>
/// <param name="sender">The object with the property.</param>
/// <param name="index">The proposed value of the property.</param>
/// <returns>The final value of the property.</returns>
private static int ValidateSelectedIndex(SelectingItemsControl sender, int index)
{
var items = sender.Items;
return (index >= 0 && index < items?.Cast<object>().Count()) ? index : -1;
}
/// <summary>
/// Coerces the <see cref="SelectedItem"/> property.
/// </summary>
/// <param name="sender">The object with the property.</param>
/// <param name="item">The proposed value of the property.</param>
/// <returns>The final value of the property.</returns>
private static object ValidateSelectedItem(SelectingItemsControl sender, object item)
{
var items = sender.Items;
return items?.Cast<object>().Contains(item) == true ? item : null;
}
/// <summary>
/// Called when new containers are initialized by the <see cref="ItemContainerGenerator"/>.
/// </summary>
@ -299,6 +295,8 @@ namespace Perspex.Controls.Primitives
/// <param name="e">The event args.</param>
private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"{this.GetType().Name} set selected index");
var index = (int)e.OldValue;
if (index != -1)

3
src/Perspex.Controls/Properties/AssemblyInfo.cs

@ -9,4 +9,5 @@ using Perspex.Metadata;
[assembly: InternalsVisibleTo("Perspex.Controls.UnitTests")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls.Primitives")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls.Primitives")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls.Shapes")]

6
src/Perspex.Controls/TextBlock.cs

@ -235,9 +235,9 @@ namespace Perspex.Controls
protected virtual FormattedText CreateFormattedText(Size constraint)
{
var result = new FormattedText(
Text,
FontFamily,
FontSize,
Text ?? string.Empty,
FontFamily ?? "Arial",
FontSize > 0 ? FontSize : 12,
FontStyle,
TextAlignment,
FontWeight);

18
src/Perspex.Controls/TextBox.cs

@ -39,6 +39,12 @@ namespace Perspex.Controls
public static readonly PerspexProperty<TextWrapping> TextWrappingProperty =
TextBlock.TextWrappingProperty.AddOwner<TextBox>();
public static readonly PerspexProperty<string> WatermarkProperty =
PerspexProperty.Register<TextBox, string>("Watermark");
public static readonly PerspexProperty<bool> UseFloatingWatermarkProperty =
PerspexProperty.Register<TextBox, bool>("UseFloatingWatermark");
private TextPresenter _presenter;
static TextBox()
@ -101,6 +107,18 @@ namespace Perspex.Controls
set { SetValue(TextProperty, value); }
}
public string Watermark
{
get { return GetValue(WatermarkProperty); }
set { SetValue(WatermarkProperty, value); }
}
public bool UseFloatingWatermark
{
get { return GetValue(UseFloatingWatermarkProperty); }
set { SetValue(UseFloatingWatermarkProperty, value); }
}
public TextWrapping TextWrapping
{
get { return GetValue(TextWrappingProperty); }

38
src/Perspex.Controls/TopLevel.cs

@ -29,13 +29,13 @@ namespace Perspex.Controls
/// Defines the <see cref="ClientSize"/> property.
/// </summary>
public static readonly PerspexProperty<Size> ClientSizeProperty =
PerspexProperty.Register<TopLevel, Size>("ClientSize");
PerspexProperty.RegisterDirect<TopLevel, Size>(nameof(ClientSize), o => o.ClientSize);
/// <summary>
/// Defines the <see cref="IsActive"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsActiveProperty =
PerspexProperty.Register<TopLevel, bool>("IsActive");
PerspexProperty.RegisterDirect<TopLevel, bool>(nameof(IsActive), o => o.IsActive);
/// <summary>
/// Defines the <see cref="IInputRoot.PointerOverElement"/> property.
@ -43,30 +43,13 @@ namespace Perspex.Controls
public static readonly PerspexProperty<IInputElement> PointerOverElementProperty =
PerspexProperty.Register<TopLevel, IInputElement>(nameof(IInputRoot.PointerOverElement));
/// <summary>
/// The render manager for the window.s
/// </summary>
private readonly IRenderManager _renderManager;
/// <summary>
/// The window renderer.
/// </summary>
private readonly IRenderer _renderer;
/// <summary>
/// The input manager for the window.
/// </summary>
private readonly IInputManager _inputManager;
/// <summary>
/// The access key handler for the window.
/// </summary>
private readonly IAccessKeyHandler _accessKeyHandler;
/// <summary>
/// The access keyboard navigation handler for the window.
/// </summary>
private readonly IKeyboardNavigationHandler _keyboardNavigationHandler;
private Size _clientSize;
private bool _isActive;
/// <summary>
/// Initializes static members of the <see cref="TopLevel"/> class.
@ -111,7 +94,7 @@ namespace Perspex.Controls
LayoutManager = TryGetService<ILayoutManager>(dependencyResolver);
_renderManager = TryGetService<IRenderManager>(dependencyResolver);
PlatformImpl.SetOwner(this);
PlatformImpl.SetInputRoot(this);
PlatformImpl.Activated = HandleActivated;
PlatformImpl.Deactivated = HandleDeactivated;
PlatformImpl.Closed = HandleClosed;
@ -177,8 +160,8 @@ namespace Perspex.Controls
/// </summary>
public Size ClientSize
{
get { return GetValue(ClientSizeProperty); }
private set { SetValue(ClientSizeProperty, value); }
get { return _clientSize; }
private set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); }
}
/// <summary>
@ -186,8 +169,8 @@ namespace Perspex.Controls
/// </summary>
public bool IsActive
{
get { return GetValue(IsActiveProperty); }
private set { SetValue(IsActiveProperty, value); }
get { return _isActive; }
private set { SetAndRaise(IsActiveProperty, ref _isActive, value); }
}
/// <summary>
@ -195,7 +178,8 @@ namespace Perspex.Controls
/// </summary>
public ILayoutManager LayoutManager
{
get; }
get;
}
/// <summary>
/// Gets the platform-specific window implementation.

77
src/Perspex.Controls/Utils/AncestorFinder.cs

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Perspex.Controls.Utils
{
public static class AncestorFinder
{
class FinderNode : IDisposable
{
private readonly IControl _control;
private readonly TypeInfo _ancestorType;
public IObservable<IControl> Observable => _subject;
private readonly Subject<IControl> _subject = new Subject<IControl>();
private FinderNode _child;
private IDisposable _disposable;
public FinderNode(IControl control, TypeInfo ancestorType)
{
_control = control;
_ancestorType = ancestorType;
}
public void Init()
{
_disposable = _control.GetObservable(Control.ParentProperty).Subscribe(OnValueChanged);
}
private void OnValueChanged(IControl next)
{
if (next == null || _ancestorType.IsAssignableFrom(next.GetType().GetTypeInfo()))
_subject.OnNext(next);
else
{
_child?.Dispose();
_child = new FinderNode(next, _ancestorType);
_child.Observable.Subscribe(OnChildValueChanged);
_child.Init();
}
}
private void OnChildValueChanged(IControl control) => _subject.OnNext(control);
public void Dispose()
{
_disposable.Dispose();
}
}
public static IObservable<IControl> Create(IControl control, Type ancestorType)
{
return new AnonymousObservable<IControl>(observer =>
{
var finder = new FinderNode(control, ancestorType.GetTypeInfo());
var subscription = finder.Observable.Subscribe(observer);
finder.Init();
return Disposable.Create(() =>
{
subscription.Dispose();
finder.Dispose();
});
});
}
}
}

2
src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs

@ -244,7 +244,7 @@ namespace TheArtOfDev.HtmlRenderer.Perspex.Adapters
x += .5;
y += .5;
}
_g.DrawRectange(((PenAdapter) pen).CreatePen(), new Rect(x, y, width, height));
_g.DrawRectangle(((PenAdapter) pen).CreatePen(), new Rect(x, y, width, height));
}
public override void DrawRectangle(RBrush brush, double x, double y, double width, double height)

4
src/Perspex.Input/IInputElement.cs

@ -3,6 +3,7 @@
using System.Diagnostics.Contracts;
using System;
using System.Collections.Generic;
using Perspex.Interactivity;
namespace Perspex.Input
@ -119,5 +120,8 @@ namespace Perspex.Input
/// <param name="p">The position, in control coordinates.</param>
/// <returns>The <see cref="IInputElement"/> at the specified position.</returns>
IInputElement InputHitTest(Point p);
List<KeyBinding> KeyBindings { get; }
}
}

18
src/Perspex.Input/InputElement.cs

@ -2,6 +2,7 @@
// 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.Interactivity;
using Perspex.Rendering;
@ -42,7 +43,7 @@ namespace Perspex.Input
/// Defines the <see cref="IsFocused"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsFocusedProperty =
PerspexProperty.Register<InputElement, bool>("IsFocused");
PerspexProperty.RegisterDirect<InputElement, bool>("IsFocused", o => o.IsFocused);
/// <summary>
/// Defines the <see cref="IsHitTestVisible"/> property.
@ -54,7 +55,7 @@ namespace Perspex.Input
/// Defines the <see cref="IsPointerOver"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsPointerOverProperty =
PerspexProperty.Register<InputElement, bool>("IsPointerOver");
PerspexProperty.RegisterDirect<InputElement, bool>("IsPointerOver", o => o.IsPointerOver);
/// <summary>
/// Defines the <see cref="GotFocus"/> event.
@ -136,6 +137,9 @@ namespace Perspex.Input
"PointerWheelChanged",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
private bool _isFocused;
private bool _isPointerOver;
/// <summary>
/// Initializes static members of the <see cref="InputElement"/> class.
/// </summary>
@ -287,8 +291,8 @@ namespace Perspex.Input
/// </summary>
public bool IsFocused
{
get { return GetValue(IsFocusedProperty); }
private set { SetValue(IsFocusedProperty, value); }
get { return _isFocused; }
private set { SetAndRaise(IsFocusedProperty, ref _isFocused, value); }
}
/// <summary>
@ -305,8 +309,8 @@ namespace Perspex.Input
/// </summary>
public bool IsPointerOver
{
get { return GetValue(IsPointerOverProperty); }
internal set { SetValue(IsPointerOverProperty, value); }
get { return _isPointerOver; }
internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); }
}
/// <summary>
@ -333,6 +337,8 @@ namespace Perspex.Input
set { SetValue(IsEnabledCoreProperty, value); }
}
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
/// <summary>
/// Returns the input element that can be found within the current control at the specified
/// position.

2
src/Perspex.Input/InputExtensions.cs

@ -11,7 +11,7 @@ namespace Perspex.Input
{
public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p)
{
Contract.Requires<NullReferenceException>(element != null);
Contract.Requires<ArgumentNullException>(element != null);
if (element.Bounds.Contains(p) &&
element.IsVisible &&

40
src/Perspex.Input/KeyBinding.cs

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Perspex.Input
{
public class KeyBinding : PerspexObject
{
public static PerspexProperty<ICommand> CommandProperty =
PerspexProperty.Register<KeyBinding, ICommand>("Command");
public ICommand Command
{
get { return GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static PerspexProperty<KeyGesture> GestureProperty =
PerspexProperty.Register<KeyBinding, KeyGesture>("Gesture");
public KeyGesture Gesture
{
get { return GetValue(GestureProperty); }
set { SetValue(GestureProperty, value); }
}
public void TryHandle(KeyEventArgs args)
{
if (Gesture?.Matches(args) == true)
{
args.Handled = true;
if (Command?.CanExecute(null) == true)
Command.Execute(null);
}
}
}
}

116
src/Perspex.Input/KeyGesture.cs

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Perspex.Input
{
public sealed class KeyGesture : IEquatable<KeyGesture>
{
public bool Equals(KeyGesture other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Key == other.Key && Modifiers == other.Modifiers;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is KeyGesture && Equals((KeyGesture) obj);
}
public override int GetHashCode()
{
unchecked
{
return ((int) Key*397) ^ (int) Modifiers;
}
}
public static bool operator ==(KeyGesture left, KeyGesture right)
{
return Equals(left, right);
}
public static bool operator !=(KeyGesture left, KeyGesture right)
{
return !Equals(left, right);
}
public Key Key { get; set; }
public InputModifiers Modifiers { get; set; }
static Dictionary<string, Key> KeySynonims = new Dictionary<string, Key>
{
{"+", Key.OemPlus },
{"-", Key.OemMinus},
{".", Key.OemPeriod }
};
//TODO: Move that to external key parser
static Key ParseKey(string key)
{
Key rv;
if (KeySynonims.TryGetValue(key.ToLower(), out rv))
return rv;
return (Key)Enum.Parse(typeof (Key), key, true);
}
static InputModifiers ParseModifier(string modifier)
{
if (modifier.Equals("ctrl", StringComparison.OrdinalIgnoreCase))
return InputModifiers.Control;
return (InputModifiers) Enum.Parse(typeof (InputModifiers), modifier, true);
}
public static KeyGesture Parse(string gesture)
{
//string.Split can't be used here because "Ctrl++" is a perfectly valid key gesture
var parts = new List<string>();
var cstart = 0;
for (var c = 0; c <= gesture.Length; c++)
{
var ch = c == gesture.Length ? '\0' : gesture[c];
if (c == gesture.Length || (ch == '+' && cstart != c))
{
parts.Add(gesture.Substring(cstart, c - cstart));
cstart = c + 1;
}
}
for (var c = 0; c < parts.Count; c++)
parts[c] = parts[c].Trim();
var rv = new KeyGesture();
for (var c = 0; c < parts.Count; c++)
{
if (c == parts.Count - 1)
rv.Key = ParseKey(parts[c]);
else
rv.Modifiers |= ParseModifier(parts[c]);
}
return rv;
}
public override string ToString()
{
var parts = new List<string>();
foreach (var flag in Enum.GetValues(typeof (InputModifiers)).Cast<InputModifiers>())
{
if (Modifiers.HasFlag(flag) && flag != InputModifiers.None)
parts.Add(flag.ToString());
}
parts.Add(Key.ToString());
return string.Join(" + ", parts);
}
public bool Matches(KeyEventArgs keyEvent) => keyEvent.Key == Key && keyEvent.Modifiers == Modifiers;
}
}

14
src/Perspex.Input/KeyboardDevice.cs

@ -104,6 +104,20 @@ namespace Perspex.Input
Source = element,
};
IVisual currentHandler = element;
while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown)
{
var bindings = (currentHandler as IInputElement)?.KeyBindings;
if(bindings!=null)
foreach (var binding in bindings)
{
if(ev.Handled)
break;
binding.TryHandle(ev);
}
currentHandler = currentHandler.VisualParent;
}
element.RaiseEvent(ev);
break;
}

2
src/Perspex.Input/Perspex.Input.csproj

@ -66,6 +66,8 @@
</Compile>
<Compile Include="Cursors.cs" />
<Compile Include="AccessKeyHandler.cs" />
<Compile Include="KeyBinding.cs" />
<Compile Include="KeyGesture.cs" />
<Compile Include="Platform\IClipboard.cs" />
<Compile Include="Platform\IStandardCursorFactory.cs" />
<Compile Include="Raw\RawTextInputEventArgs.cs" />

2
src/Perspex.Input/Properties/AssemblyInfo.cs

@ -2,5 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Reflection;
using Perspex.Metadata;
[assembly: AssemblyTitle("Perspex.Input")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Input")]

18
src/Perspex.Interactivity/Interactive.cs

@ -4,11 +4,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Perspex.Layout;
using Perspex.VisualTree;
namespace Perspex.Interactivity
{
@ -39,8 +37,8 @@ namespace Perspex.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false)
{
Contract.Requires<NullReferenceException>(routedEvent != null);
Contract.Requires<NullReferenceException>(handler != null);
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
List<EventSubscription> subscriptions;
@ -87,8 +85,8 @@ namespace Perspex.Interactivity
/// <param name="handler">The handler.</param>
public void RemoveHandler(RoutedEvent routedEvent, Delegate handler)
{
Contract.Requires<NullReferenceException>(routedEvent != null);
Contract.Requires<NullReferenceException>(handler != null);
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
List<EventSubscription> subscriptions;
@ -116,7 +114,7 @@ namespace Perspex.Interactivity
/// <param name="e">The event args.</param>
public void RaiseEvent(RoutedEventArgs e)
{
Contract.Requires<NullReferenceException>(e != null);
Contract.Requires<ArgumentNullException>(e != null);
e.Source = e.Source ?? this;
@ -143,7 +141,7 @@ namespace Perspex.Interactivity
/// <param name="e">The event args.</param>
private void BubbleEvent(RoutedEventArgs e)
{
Contract.Requires<NullReferenceException>(e != null);
Contract.Requires<ArgumentNullException>(e != null);
e.Route = RoutingStrategies.Bubble;
@ -159,7 +157,7 @@ namespace Perspex.Interactivity
/// <param name="e">The event args.</param>
private void TunnelEvent(RoutedEventArgs e)
{
Contract.Requires<NullReferenceException>(e != null);
Contract.Requires<ArgumentNullException>(e != null);
e.Route = RoutingStrategies.Tunnel;
@ -175,7 +173,7 @@ namespace Perspex.Interactivity
/// <param name="e">The event args.</param>
private void RaiseEventImpl(RoutedEventArgs e)
{
Contract.Requires<NullReferenceException>(e != null);
Contract.Requires<ArgumentNullException>(e != null);
e.RoutedEvent.InvokeClassHandlers(this, e);

14
src/Perspex.Interactivity/RoutedEvent.cs

@ -26,9 +26,9 @@ namespace Perspex.Interactivity
Type eventArgsType,
Type ownerType)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(eventArgsType != null);
Contract.Requires<NullReferenceException>(ownerType != null);
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(eventArgsType != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<InvalidCastException>(typeof(RoutedEventArgs).GetTypeInfo().IsAssignableFrom(eventArgsType.GetTypeInfo()));
EventArgsType = eventArgsType;
@ -66,7 +66,7 @@ namespace Perspex.Interactivity
RoutingStrategies routingStrategy)
where TEventArgs : RoutedEventArgs
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<ArgumentNullException>(name != null);
return new RoutedEvent<TEventArgs>(name, routingStrategy, typeof(TOwner));
}
@ -77,7 +77,7 @@ namespace Perspex.Interactivity
Type ownerType)
where TEventArgs : RoutedEventArgs
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<ArgumentNullException>(name != null);
return new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType);
}
@ -117,8 +117,8 @@ namespace Perspex.Interactivity
public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType)
: base(name, routingStrategies, typeof(TEventArgs), ownerType)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(ownerType != null);
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
}
public void AddClassHandler<TTarget>(

7
src/Perspex.SceneGraph/Media/FormattedText.cs

@ -29,9 +29,10 @@ namespace Perspex.Media
TextAlignment textAlignment,
FontWeight fontWeight)
{
//TODO: Find out why it was null in the first place. Demo project - AvalonStudio
//https://github.com/VitalElement/AvalonStudio/commit/787fb9396feb74e6ca6bd4e08436269a349df9c6
text = text ?? "";
Contract.Requires<ArgumentNullException>(text != null);
Contract.Requires<ArgumentNullException>(fontFamilyName != null);
Contract.Requires<ArgumentException>(fontSize > 0);
Text = text;
FontFamilyName = fontFamilyName;
FontSize = fontSize;

2
src/Perspex.SceneGraph/Media/IDrawingContext.cs

@ -47,7 +47,7 @@ namespace Perspex.Media
/// <param name="pen">The pen.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param>
void DrawRectange(Pen pen, Rect rect, float cornerRadius = 0.0f);
void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f);
/// <summary>
/// Draws text.

21
src/Perspex.SceneGraph/Visual.cs

@ -32,7 +32,7 @@ namespace Perspex
/// Defines the <see cref="Bounds"/> property.
/// </summary>
public static readonly PerspexProperty<Rect> BoundsProperty =
PerspexProperty.Register<Visual, Rect>(nameof(Bounds));
PerspexProperty.RegisterDirect<Visual, Rect>(nameof(Bounds), o => o.Bounds);
/// <summary>
/// Defines the <see cref="ClipToBounds"/> property.
@ -64,6 +64,12 @@ namespace Perspex
public static readonly PerspexProperty<RelativePoint> TransformOriginProperty =
PerspexProperty.Register<Visual, RelativePoint>(nameof(TransformOrigin), defaultValue: RelativePoint.Center);
/// <summary>
/// Defines the <see cref="IVisual.VisualParent"/> property.
/// </summary>
public static readonly PerspexProperty<IVisual> VisualParentProperty =
PerspexProperty.RegisterDirect<Visual, IVisual>("VisualParent", o => o._visualParent);
/// <summary>
/// Defines the <see cref="ZIndex"/> property.
/// </summary>
@ -75,10 +81,15 @@ namespace Perspex
/// </summary>
private readonly PerspexList<IVisual> _visualChildren;
/// <summary>
/// The visual's bounds relative to its parent.
/// </summary>
private Rect _bounds;
/// <summary>
/// Holds the parent of the visual.
/// </summary>
private Visual _visualParent;
private IVisual _visualParent;
/// <summary>
/// Whether the element is attached to the visual tree.
@ -121,8 +132,8 @@ namespace Perspex
/// </summary>
public Rect Bounds
{
get { return GetValue(BoundsProperty); }
protected set { SetValue(BoundsProperty, value); }
get { return _bounds; }
protected set { SetAndRaise(BoundsProperty, ref _bounds, value); }
}
/// <summary>
@ -439,6 +450,8 @@ namespace Perspex
{
NotifyAttachedToVisualTree(newRoot);
}
RaisePropertyChanged(VisualParentProperty, old, value, BindingPriority.LocalValue);
}
}

8
src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs

@ -19,7 +19,7 @@ namespace Perspex.VisualTree
/// <returns>The visual's ancestors.</returns>
public static IEnumerable<IVisual> GetVisualAncestors(this IVisual visual)
{
Contract.Requires<NullReferenceException>(visual != null);
Contract.Requires<ArgumentNullException>(visual != null);
visual = visual.VisualParent;
@ -53,7 +53,7 @@ namespace Perspex.VisualTree
/// <returns>The visuals at the requested point.</returns>
public static IVisual GetVisualAt(this IVisual visual, Point p)
{
Contract.Requires<NullReferenceException>(visual != null);
Contract.Requires<ArgumentNullException>(visual != null);
return visual.GetVisualsAt(p).FirstOrDefault();
}
@ -66,7 +66,7 @@ namespace Perspex.VisualTree
/// <returns>The visuals at the requested point.</returns>
public static IEnumerable<IVisual> GetVisualsAt(this IVisual visual, Point p)
{
Contract.Requires<NullReferenceException>(visual != null);
Contract.Requires<ArgumentNullException>(visual != null);
if (visual.Bounds.Contains(p))
{
@ -162,7 +162,7 @@ namespace Perspex.VisualTree
/// </returns>
public static IVisual GetVisualRoot(this IVisual visual)
{
Contract.Requires<NullReferenceException>(visual != null);
Contract.Requires<ArgumentNullException>(visual != null);
var parent = visual.VisualParent;

2
src/Perspex.Styling/LogicalTree/LogicalExtensions.cs

@ -11,7 +11,7 @@ namespace Perspex.LogicalTree
{
public static IEnumerable<ILogical> GetLogicalAncestors(this ILogical logical)
{
Contract.Requires<NullReferenceException>(logical != null);
Contract.Requires<ArgumentNullException>(logical != null);
logical = logical.LogicalParent;

2
src/Perspex.Styling/Styling/StyleBinding.cs

@ -86,7 +86,7 @@ namespace Perspex.Styling
/// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns>
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
Contract.Requires<NullReferenceException>(observer != null);
Contract.Requires<ArgumentNullException>(observer != null);
if (Source == null)
{

53
src/Perspex.Themes.Default/TextBoxStyle.cs

@ -8,6 +8,7 @@ using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
using Perspex.Media;
using Perspex.Styling;
using System.Reactive.Linq;
namespace Perspex.Themes.Default
{
@ -58,21 +59,49 @@ namespace Perspex.Themes.Default
[~Border.BackgroundProperty] = control[~TemplatedControl.BackgroundProperty],
[~Border.BorderBrushProperty] = control[~TemplatedControl.BorderBrushProperty],
[~Border.BorderThicknessProperty] = control[~TemplatedControl.BorderThicknessProperty],
Child = new ScrollViewer
Child = new StackPanel
{
[~ScrollViewer.CanScrollHorizontallyProperty] = control[~ScrollViewer.CanScrollHorizontallyProperty],
[~ScrollViewer.HorizontalScrollBarVisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty],
[~ScrollViewer.VerticalScrollBarVisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty],
Content = new TextPresenter
Children = new Controls.Controls
{
Name = "textPresenter",
[~TextPresenter.CaretIndexProperty] = control[~TextBox.CaretIndexProperty],
[~TextPresenter.SelectionStartProperty] = control[~TextBox.SelectionStartProperty],
[~TextPresenter.SelectionEndProperty] = control[~TextBox.SelectionEndProperty],
[~TextBlock.TextProperty] = control[~TextBox.TextProperty],
[~TextBlock.TextWrappingProperty] = control[~TextBox.TextWrappingProperty],
new TextBlock
{
Name = "floatingWatermark",
Foreground = SolidColorBrush.Parse("#007ACC"),
FontSize = 10,
[~TextBlock.TextProperty] = control[~TextBox.WatermarkProperty],
[~TextBlock.IsVisibleProperty] = control[~TextBox.TextProperty].Cast<string>().Select(x => (object)(!string.IsNullOrEmpty(x) && control.UseFloatingWatermark))
},
new Panel
{
Children = new Controls.Controls
{
new TextBlock
{
Name = "watermark",
Opacity = 0.5,
[~TextBlock.TextProperty] = control[~TextBox.WatermarkProperty],
[~TextBlock.IsVisibleProperty] = control[~TextBox.TextProperty].Cast<string>().Select(x => (object)string.IsNullOrEmpty(x))
},
new ScrollViewer
{
[~ScrollViewer.CanScrollHorizontallyProperty] = control[~ScrollViewer.CanScrollHorizontallyProperty],
[~ScrollViewer.HorizontalScrollBarVisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty],
[~ScrollViewer.VerticalScrollBarVisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty],
Content = new TextPresenter
{
Name = "textPresenter",
[~TextPresenter.CaretIndexProperty] = control[~TextBox.CaretIndexProperty],
[~TextPresenter.SelectionStartProperty] = control[~TextBox.SelectionStartProperty],
[~TextPresenter.SelectionEndProperty] = control[~TextBox.SelectionEndProperty],
[~TextBlock.TextProperty] = control[~TextBox.TextProperty],
[~TextBlock.TextWrappingProperty] = control[~TextBox.TextWrappingProperty],
}
}
}
}
}
}
},
};
return result;

2
src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs

@ -143,7 +143,7 @@ namespace Perspex.Direct2D1.Media
/// <param name="pen">The pen.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param>
public void DrawRectange(Pen pen, Rect rect, float cornerRadius)
public void DrawRectangle(Pen pen, Rect rect, float cornerRadius)
{
using (var brush = CreateBrush(pen.Brush, rect.Size))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_renderTarget))

4
src/Windows/Perspex.Direct2D1/Media/Imaging/BitmapImpl.cs

@ -67,12 +67,12 @@ namespace Perspex.Direct2D1.Media
/// <summary>
/// Gets the width of the bitmap, in pixels.
/// </summary>
public int PixelWidth => WicImpl.Size.Height;
public int PixelWidth => WicImpl.Size.Width;
/// <summary>
/// Gets the height of the bitmap, in pixels.
/// </summary>
public int PixelHeight => WicImpl.Size.Width;
public int PixelHeight => WicImpl.Size.Height;
/// <summary>
/// Gets the WIC implementation of the bitmap.

16
src/Windows/Perspex.Win32/WindowImpl.cs

@ -31,10 +31,12 @@ namespace Perspex.Win32
private IntPtr _hwnd;
private TopLevel _owner;
private IInputRoot _owner;
private bool _trackingMouse;
private bool _isActive;
public WindowImpl()
{
CreateWindow();
@ -163,9 +165,9 @@ namespace Perspex.Win32
return new Point(p.X, p.Y);
}
public void SetOwner(TopLevel owner)
public void SetInputRoot(IInputRoot inputRoot)
{
_owner = owner;
_owner = inputRoot;
}
public void SetTitle(string title)
@ -181,13 +183,13 @@ namespace Perspex.Win32
public virtual IDisposable ShowDialog()
{
var disabled = s_instances.Where(x => x != this && x.IsEnabled).ToList();
TopLevel activated = null;
WindowImpl activated = null;
foreach (var window in disabled)
{
if (window._owner.IsActive)
if (window._isActive)
{
activated = window._owner;
activated = window;
}
window.IsEnabled = false;
@ -253,6 +255,7 @@ namespace Perspex.Win32
{
case UnmanagedMethods.WindowActivate.WA_ACTIVE:
case UnmanagedMethods.WindowActivate.WA_CLICKACTIVE:
_isActive = true;
if (Activated != null)
{
Activated();
@ -261,6 +264,7 @@ namespace Perspex.Win32
break;
case UnmanagedMethods.WindowActivate.WA_INACTIVE:
_isActive = false;
if (Deactivated != null)
{
Deactivated();

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

@ -73,6 +73,7 @@
<Compile Include="Collections\PerspexDictionaryTests.cs" />
<Compile Include="Collections\PerspexListTests.cs" />
<Compile Include="Collections\PropertyChangedTracker.cs" />
<Compile Include="PerspexObjectTests_Direct.cs" />
<Compile Include="PerspexObjectTests_GetObservable.cs" />
<Compile Include="PerspexObjectTests_Validation.cs" />
<Compile Include="PerspexObjectTests_Binding.cs" />

4
tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs

@ -39,7 +39,7 @@ namespace Perspex.Base.UnitTests
{
Class1 target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
Assert.Throws<ArgumentException>(() =>
{
target.Bind(Class2.BarProperty, Observable.Return("foo"));
});
@ -212,7 +212,7 @@ namespace Perspex.Base.UnitTests
{
Class1 target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
Assert.Throws<ArgumentException>(() =>
{
target[Class1.FooProperty] = Observable.Return("newvalue");
});

332
tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs

@ -0,0 +1,332 @@
// 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.Reactive.Subjects;
using Xunit;
namespace Perspex.Base.UnitTests
{
public class PerspexObjectTests_Direct
{
[Fact]
public void GetValue_Gets_Value()
{
var target = new Class1();
Assert.Equal("initial", target.GetValue(Class1.FooProperty));
}
[Fact]
public void GetValue_Gets_Value_NonGeneric()
{
var target = new Class1();
Assert.Equal("initial", target.GetValue((PerspexProperty)Class1.FooProperty));
}
[Fact]
public void GetValue_On_Unregistered_Property_Throws_Exception()
{
var target = new Class2();
Assert.Throws<ArgumentException>(() => target.GetValue(Class1.BarProperty));
}
[Fact]
public void SetValue_Sets_Value()
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "newvalue");
Assert.Equal("newvalue", target.Foo);
}
[Fact]
public void SetValue_Sets_Value_NonGeneric()
{
var target = new Class1();
target.SetValue((PerspexProperty)Class1.FooProperty, "newvalue");
Assert.Equal("newvalue", target.Foo);
}
[Fact]
public void SetValue_Raises_PropertyChanged()
{
var target = new Class1();
bool raised = false;
target.PropertyChanged += (s, e) =>
raised = e.Property == Class1.FooProperty &&
(string)e.OldValue == "initial" &&
(string)e.NewValue == "newvalue" &&
e.Priority == BindingPriority.LocalValue;
target.SetValue(Class1.FooProperty, "newvalue");
Assert.True(raised);
}
[Fact]
public void SetValue_Raises_Changed()
{
var target = new Class1();
bool raised = false;
Class1.FooProperty.Changed.Subscribe(e =>
raised = e.Property == Class1.FooProperty &&
(string)e.OldValue == "initial" &&
(string)e.NewValue == "newvalue" &&
e.Priority == BindingPriority.LocalValue);
target.SetValue(Class1.FooProperty, "newvalue");
Assert.True(raised);
}
[Fact]
public void SetValue_On_Unregistered_Property_Throws_Exception()
{
var target = new Class2();
Assert.Throws<ArgumentException>(() => target.SetValue(Class1.BarProperty, "value"));
}
[Fact]
public void GetObservable_Returns_Values()
{
var target = new Class1();
List<string> values = new List<string>();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
target.Foo = "newvalue";
Assert.Equal(new[] { "initial", "newvalue" }, values);
}
[Fact]
public void Bind_Binds_Property_Value()
{
var target = new Class1();
var source = new Subject<string>();
var sub = target.Bind(Class1.FooProperty, source);
Assert.Equal("initial", target.Foo);
source.OnNext("first");
Assert.Equal("first", target.Foo);
source.OnNext("second");
Assert.Equal("second", target.Foo);
sub.Dispose();
source.OnNext("third");
Assert.Equal("second", target.Foo);
}
[Fact]
public void Bind_Binds_Property_Value_NonGeneric()
{
var target = new Class1();
var source = new Subject<string>();
var sub = target.Bind((PerspexProperty)Class1.FooProperty, source);
Assert.Equal("initial", target.Foo);
source.OnNext("first");
Assert.Equal("first", target.Foo);
source.OnNext("second");
Assert.Equal("second", target.Foo);
sub.Dispose();
source.OnNext("third");
Assert.Equal("second", target.Foo);
}
[Fact]
public void ReadOnly_Property_Cannot_Be_Set()
{
var target = new Class1();
Assert.Throws<ArgumentException>(() =>
target.SetValue(Class1.BarProperty, "newvalue"));
}
[Fact]
public void ReadOnly_Property_Cannot_Be_Set_NonGeneric()
{
var target = new Class1();
Assert.Throws<ArgumentException>(() =>
target.SetValue((PerspexProperty)Class1.BarProperty, "newvalue"));
}
[Fact]
public void ReadOnly_Property_Cannot_Be_Bound()
{
var target = new Class1();
var source = new Subject<string>();
Assert.Throws<ArgumentException>(() =>
target.Bind(Class1.BarProperty, source));
}
[Fact]
public void ReadOnly_Property_Cannot_Be_Bound_NonGeneric()
{
var target = new Class1();
var source = new Subject<string>();
Assert.Throws<ArgumentException>(() =>
target.Bind(Class1.BarProperty, source));
}
[Fact]
public void GetValue_Gets_Value_On_AddOwnered_Property()
{
var target = new Class2();
Assert.Equal("initial2", target.GetValue(Class2.FooProperty));
}
[Fact]
public void GetValue_Gets_Value_On_AddOwnered_Property_Using_Original()
{
var target = new Class2();
Assert.Equal("initial2", target.GetValue(Class1.FooProperty));
}
[Fact]
public void GetValue_Gets_Value_On_AddOwnered_Property_Using_Original_NonGeneric()
{
var target = new Class2();
Assert.Equal("initial2", target.GetValue((PerspexProperty)Class1.FooProperty));
}
[Fact]
public void SetValue_Sets_Value_On_AddOwnered_Property_Using_Original()
{
var target = new Class2();
target.SetValue(Class1.FooProperty, "newvalue");
Assert.Equal("newvalue", target.Foo);
}
[Fact]
public void SetValue_Sets_Value_On_AddOwnered_Property_Using_Original_NonGeneric()
{
var target = new Class2();
target.SetValue((PerspexProperty)Class1.FooProperty, "newvalue");
Assert.Equal("newvalue", target.Foo);
}
[Fact]
public void Bind_Binds_AddOwnered_Property_Value()
{
var target = new Class2();
var source = new Subject<string>();
var sub = target.Bind(Class1.FooProperty, source);
Assert.Equal("initial2", target.Foo);
source.OnNext("first");
Assert.Equal("first", target.Foo);
source.OnNext("second");
Assert.Equal("second", target.Foo);
sub.Dispose();
source.OnNext("third");
Assert.Equal("second", target.Foo);
}
[Fact]
public void Bind_Binds_AddOwnered_Property_Value_NonGeneric()
{
var target = new Class2();
var source = new Subject<string>();
var sub = target.Bind((PerspexProperty)Class1.FooProperty, source);
Assert.Equal("initial2", target.Foo);
source.OnNext("first");
Assert.Equal("first", target.Foo);
source.OnNext("second");
Assert.Equal("second", target.Foo);
sub.Dispose();
source.OnNext("third");
Assert.Equal("second", target.Foo);
}
[Fact]
public void Property_Notifies_Initialized()
{
Class1 target;
bool raised = false;
Class1.FooProperty.Initialized.Subscribe(e =>
raised = e.Property == Class1.FooProperty &&
e.OldValue == PerspexProperty.UnsetValue &&
(string)e.NewValue == "initial" &&
e.Priority == BindingPriority.Unset);
target = new Class1();
Assert.True(raised);
}
private class Class1 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
PerspexProperty.RegisterDirect<Class1, string>("Foo", o => o.Foo, (o, v) => o.Foo = v);
public static readonly PerspexProperty<string> BarProperty =
PerspexProperty.RegisterDirect<Class1, string>("Bar", o => o.Bar);
private string _foo = "initial";
private string _bar = "bar";
public string Foo
{
get { return _foo; }
set { SetAndRaise(FooProperty, ref _foo, value); }
}
public string Bar
{
get { return _bar; }
}
}
private class Class2 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
Class1.FooProperty.AddOwner<Class2>(o => o.Foo, (o, v) => o.Foo = v);
private string _foo = "initial2";
static Class2()
{
}
public string Foo
{
get { return _foo; }
set { SetAndRaise(FooProperty, ref _foo, value); }
}
}
}
}

13
tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.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;
using Xunit;
namespace Perspex.Base.UnitTests
@ -44,6 +45,14 @@ namespace Perspex.Base.UnitTests
Assert.Equal("changed", child.GetValue(Class1.BazProperty));
}
[Fact]
public void GetValue_Throws_Exception_For_Unregistered_Property()
{
var target = new Class3();
Assert.Throws<ArgumentException>(() => target.GetValue(Class1.FooProperty));
}
private class Class1 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
@ -66,5 +75,9 @@ namespace Perspex.Base.UnitTests
set { InheritanceParent = value; }
}
}
private class Class3 : PerspexObject
{
}
}
}

4
tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs

@ -87,7 +87,7 @@ namespace Perspex.Base.UnitTests
{
Class1 target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
Assert.Throws<ArgumentException>(() =>
{
target.SetValue(Class2.BarProperty, "invalid");
});
@ -98,7 +98,7 @@ namespace Perspex.Base.UnitTests
{
Class1 target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
Assert.Throws<ArgumentException>(() =>
{
target.SetValue(Class1.FooProperty, 123);
});

65
tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs

@ -125,6 +125,69 @@ namespace Perspex.Base.UnitTests
Assert.Equal("newvalue", value);
}
[Fact]
public void IsDirect_Property_Set_On_Direct_PerspexProperty()
{
PerspexProperty<string> target = new PerspexProperty<string>(
"test",
typeof(Class1),
o => null,
(o, v) => { });
Assert.True(target.IsDirect);
}
[Fact]
public void Property_Equals_Should_Handle_Null()
{
var p1 = new PerspexProperty<string>("p1", typeof(Class1));
Assert.NotEqual(p1, null);
Assert.NotEqual(null, p1);
Assert.False(p1 == null);
Assert.False(null == p1);
Assert.False(p1.Equals(null));
Assert.True((PerspexProperty)null == (PerspexProperty)null);
}
[Fact]
public void AddOwnered_Property_Should_Equal_Original()
{
var p1 = new PerspexProperty<string>("p1", typeof(Class1));
var p2 = p1.AddOwner<Class3>();
Assert.Equal(p1, p2);
Assert.Equal(p1.GetHashCode(), p2.GetHashCode());
Assert.True(p1 == p2);
}
[Fact]
public void AddOwnered_Direct_Property_Should_Equal_Original()
{
var p1 = new PerspexProperty<string>("d1", typeof(Class1), o => null, (o,v) => { });
var p2 = p1.AddOwner<Class3>(o => null, (o, v) => { });
Assert.Equal(p1, p2);
Assert.Equal(p1.GetHashCode(), p2.GetHashCode());
Assert.True(p1 == p2);
}
[Fact]
public void AddOwner_With_Getter_And_Setter_On_Standard_Property_Should_Throw()
{
var p1 = new PerspexProperty<string>("p1", typeof(Class1));
Assert.Throws<InvalidOperationException>(() => p1.AddOwner<Class3>(o => null, (o, v) => { }));
}
[Fact]
public void AddOwner_On_Direct_Property_Without_Getter_Or_Setter_Should_Throw()
{
var p1 = new PerspexProperty<string>("e1", typeof(Class1), o => null, (o, v) => { });
Assert.Throws<InvalidOperationException>(() => p1.AddOwner<Class3>());
}
private class Class1 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
@ -135,7 +198,7 @@ namespace Perspex.Base.UnitTests
{
}
private class Class3
private class Class3 : PerspexObject
{
}
}

14
tests/Perspex.Base.UnitTests/PriorityValueTests.cs

@ -47,7 +47,7 @@ namespace Perspex.Base.UnitTests
var target = new PriorityValue("Test", typeof(string));
target.Add(Single("foo"), 0);
target.SetDirectValue("bar", 0);
target.SetValue("bar", 0);
Assert.Equal("bar", target.Value);
}
@ -60,7 +60,7 @@ namespace Perspex.Base.UnitTests
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 0);
target.SetValue("first", 0);
Assert.Equal("first", target.Value);
source.OnNext("second");
Assert.Equal("second", target.Value);
@ -76,7 +76,7 @@ namespace Perspex.Base.UnitTests
target.Add(nonActive, 0);
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 0);
target.SetValue("first", 0);
Assert.Equal("first", target.Value);
nonActive.OnNext("second");
Assert.Equal("second", target.Value);
@ -92,7 +92,7 @@ namespace Perspex.Base.UnitTests
target.Add(nonActive, 1);
target.Add(source, 1);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 1);
target.SetValue("first", 1);
Assert.Equal("first", target.Value);
nonActive.OnNext("second");
Assert.Equal("first", target.Value);
@ -106,7 +106,7 @@ namespace Perspex.Base.UnitTests
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 0);
target.SetValue("first", 0);
Assert.Equal("first", target.Value);
source.OnNext("second");
Assert.Equal("second", target.Value);
@ -267,9 +267,9 @@ namespace Perspex.Base.UnitTests
{
var target = new PriorityValue("Test", typeof(int), x => Math.Min((int)x, 10));
target.SetDirectValue(5, 0);
target.SetValue(5, 0);
Assert.Equal(5, target.Value);
target.SetDirectValue(15, 0);
target.SetValue(15, 0);
Assert.Equal(10, target.Value);
}

18
tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs

@ -4,6 +4,7 @@
using System.Collections.Specialized;
using System.Linq;
using Perspex.Controls.Presenters;
using Perspex.LogicalTree;
using Xunit;
namespace Perspex.Controls.UnitTests
@ -29,11 +30,28 @@ namespace Perspex.Controls.UnitTests
var child = new Control();
target.Content = child;
target.ApplyTemplate();
target.Content = null;
target.ApplyTemplate();
Assert.Equal(new ILogical[0], ((ILogical)target).LogicalChildren.ToList());
}
[Fact]
public void Clearing_Content_Clear_Childs_Parent()
{
var target = new ContentPresenter();
var child = new Control();
target.Content = child;
target.ApplyTemplate();
target.Content = null;
target.ApplyTemplate();
Assert.Null(child.Parent);
Assert.Null(child.GetLogicalParent());
}
[Fact]
public void Changing_Content_Should_Fire_LogicalChildren_CollectionChanged()
{

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

@ -112,6 +112,8 @@
<Compile Include="Templates\TemplateExtensionsTests.cs" />
<Compile Include="TestTemplatedControl.cs" />
<Compile Include="TestRoot.cs" />
<Compile Include="Utils\AncestorFinderTests.cs" />
<Compile Include="Utils\HotKeyManagerTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Perspex.Animation\Perspex.Animation.csproj">

1
tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs

@ -33,6 +33,7 @@ namespace Perspex.Controls.UnitTests.Primitives
target.ApplyTemplate();
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(target.Items.Cast<TabItem>().First(), target.SelectedItem);
Assert.Equal(target.Items.Cast<TabItem>().First(), target.SelectedTab);
}

5
tests/Perspex.Controls.UnitTests/TabControlTests.cs

@ -139,6 +139,7 @@ namespace Perspex.Controls.UnitTests
new Item("Bar"),
new TextBlock { Text = "Baz" },
new TabItem { Content = "Qux" },
new TabItem { Content = new TextBlock { Text = "Bob" } }
};
var target = new TabControl
@ -168,6 +169,10 @@ namespace Perspex.Controls.UnitTests
target.SelectedIndex = 3;
dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext;
Assert.Equal("Qux", dataContext);
target.SelectedIndex = 4;
dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext;
Assert.Equal("Base", dataContext);
}
private Control CreateTabControlTemplate(TabControl parent)

40
tests/Perspex.Controls.UnitTests/Utils/AncestorFinderTests.cs

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Perspex.Controls.Utils;
using Xunit;
namespace Perspex.Controls.UnitTests.Utils
{
public class AncestorFinderTests
{
[Fact]
public void SanityCheck()
{
var child = new Control();
var parent = new Decorator();
var grandParent = new Border();
var grandParent2 = new Border();
IVisual currentParent = null;
var subscription = AncestorFinder.Create(child, typeof (Border)).Subscribe(s => currentParent = s);
Assert.Null(currentParent);
parent.Child = child;
Assert.Null(currentParent);
grandParent.Child = parent;
Assert.Equal(grandParent, currentParent);
grandParent.Child = null;
grandParent2.Child = parent;
Assert.Equal(grandParent2, currentParent);
subscription.Dispose();
parent.Child = null;
Assert.Equal(grandParent2, currentParent);
}
}
}

74
tests/Perspex.Controls.UnitTests/Utils/HotKeyManagerTests.cs

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Moq;
using Perspex.Controls.Presenters;
using Perspex.Controls.Templates;
using Perspex.Input;
using Perspex.Platform;
using Perspex.Styling;
using Xunit;
namespace Perspex.Controls.UnitTests.Utils
{
public class HotKeyManagerTests
{
[Fact]
public void HotKeyManager_Should_Register_And_Unregister_Key_Binding()
{
using (PerspexLocator.EnterScope())
{
var windowImpl = new Mock<IWindowImpl>();
var styler = new Mock<Styler>();
PerspexLocator.CurrentMutable
.Bind<IWindowImpl>().ToConstant(windowImpl.Object)
.Bind<IStyler>().ToConstant(styler.Object);
var gesture1 = new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control};
var gesture2 = new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Control};
var tl = new Window();
var button = new Button();
tl.Content = button;
tl.Template = CreateWindowTemplate();
tl.ApplyTemplate();
HotKeyManager.SetHotKey(button, gesture1);
Assert.Equal(gesture1, tl.KeyBindings[0].Gesture);
HotKeyManager.SetHotKey(button, gesture2);
Assert.Equal(gesture2, tl.KeyBindings[0].Gesture);
tl.Content = null;
tl.Presenter.ApplyTemplate();
Assert.Empty(tl.KeyBindings);
tl.Content = button;
tl.Presenter.ApplyTemplate();
Assert.Equal(gesture2, tl.KeyBindings[0].Gesture);
HotKeyManager.SetHotKey(button, null);
Assert.Empty(tl.KeyBindings);
}
}
private ControlTemplate CreateWindowTemplate()
{
return new ControlTemplate<Window>(parent =>
{
return new ContentPresenter
{
Name = "contentPresenter",
[~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty],
};
});
}
}
}

28
tests/Perspex.Input.UnitTests/KeyGestureParseTests.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Perspex.Input.UnitTests
{
public class KeyGestureParseTests
{
private static readonly Dictionary<string, KeyGesture> SampleData = new Dictionary<string, KeyGesture>
{
{"Ctrl+A", new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control}},
{" \tShift\t+Alt +B", new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Shift|InputModifiers.Alt} },
{"Control++", new KeyGesture {Key = Key.OemPlus, Modifiers = InputModifiers.Control} }
};
[Fact]
public void Key_Gesture_Is_Able_To_Parse_Sample_Data()
{
foreach (var d in SampleData)
Assert.Equal(d.Value, KeyGesture.Parse(d.Key));
}
}
}

1
tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj

@ -57,6 +57,7 @@
<ItemGroup>
<Compile Include="KeyboardNavigationTests_Arrows.cs" />
<Compile Include="KeyboardNavigationTests_Tab.cs" />
<Compile Include="KeyGestureParseTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>

148
tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs

@ -0,0 +1,148 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using Perspex.Markup.Binding;
using Xunit;
namespace Perspex.Markup.UnitTests.Binding
{
public class ExpressionNodeBuilderTests
{
[Fact]
public void Should_Build_Single_Property()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo"));
AssertIsProperty(result[0], "Foo");
}
[Fact]
public void Should_Build_Underscored_Property()
{
var result = ToList(ExpressionNodeBuilder.Build("_Foo"));
AssertIsProperty(result[0], "_Foo");
}
[Fact]
public void Should_Build_Property_Chain()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz"));
Assert.Equal(3, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsProperty(result[1], "Bar");
AssertIsProperty(result[2], "Baz");
}
[Fact]
public void Should_Build_Negated_Property_Chain()
{
var result = ToList(ExpressionNodeBuilder.Build("!Foo.Bar.Baz"));
Assert.Equal(4, result.Count);
Assert.IsType<LogicalNotNode>(result[0]);
AssertIsProperty(result[1], "Foo");
AssertIsProperty(result[2], "Bar");
AssertIsProperty(result[3], "Baz");
}
[Fact]
public void Should_Build_Double_Negated_Property_Chain()
{
var result = ToList(ExpressionNodeBuilder.Build("!!Foo.Bar.Baz"));
Assert.Equal(5, result.Count);
Assert.IsType<LogicalNotNode>(result[0]);
Assert.IsType<LogicalNotNode>(result[1]);
AssertIsProperty(result[2], "Foo");
AssertIsProperty(result[3], "Bar");
AssertIsProperty(result[4], "Baz");
}
[Fact]
public void Should_Build_Indexed_Property()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[15]"));
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], 15);
Assert.IsType<IndexerNode>(result[1]);
}
[Fact]
public void Should_Build_Multiple_Indexed_Property()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[15,6]"));
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], 15, 6);
}
[Fact]
public void Should_Build_Multiple_Indexed_Property_With_Space()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[5, 16]"));
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], 5, 16);
}
[Fact]
public void Should_Build_Consecutive_Indexers()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[15][16]"));
Assert.Equal(3, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], 15);
AssertIsIndexer(result[2], 16);
}
[Fact]
public void Should_Build_Indexed_Property_In_Chain()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar[5, 6].Baz"));
Assert.Equal(4, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsProperty(result[1], "Bar");
AssertIsIndexer(result[2], 5, 6);
AssertIsProperty(result[3], "Baz");
}
private void AssertIsProperty(ExpressionNode node, string name)
{
Assert.IsType<PropertyAccessorNode>(node);
var p = (PropertyAccessorNode)node;
Assert.Equal(name, p.PropertyName);
}
private void AssertIsIndexer(ExpressionNode node, params object[] args)
{
Assert.IsType<IndexerNode>(node);
var e = (IndexerNode)node;
Assert.Equal(e.Arguments.ToArray(), args.ToArray());
}
private List<ExpressionNode> ToList(ExpressionNode node)
{
var result = new List<ExpressionNode>();
while (node != null)
{
result.Add(node);
node = node.Next;
}
return result;
}
}
}

74
tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs

@ -0,0 +1,74 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Markup.Binding;
using Xunit;
namespace Perspex.Markup.UnitTests.Binding
{
public class ExpressionNodeBuilderTests_Errors
{
[Fact]
public void Identifier_Cannot_Start_With_Digit()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("1Foo"));
}
[Fact]
public void Identifier_Cannot_Start_With_Symbol()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.%Bar"));
}
[Fact]
public void Expression_Cannot_End_With_Period()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar."));
}
[Fact]
public void Expression_Cannot_Have_Empty_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[]"));
}
[Fact]
public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[,3,4]"));
}
[Fact]
public void Expression_Cannot_Have_Extra_Comma_In_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,,4]"));
}
[Fact]
public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4,]"));
}
[Fact]
public void Expression_Cannot_Have_Digit_After_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]5"));
}
[Fact]
public void Expression_Cannot_Have_Letter_After_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]A"));
}
}
}

113
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs

@ -0,0 +1,113 @@
// 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.Collections.ObjectModel;
using System.Reactive.Linq;
using Perspex.Markup.Binding;
using Xunit;
namespace Perspex.Markup.UnitTests.Binding
{
public class ExpressionObserverTests_Indexer
{
[Fact]
public async void Should_Get_Array_Value()
{
var data = new { Foo = new [] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal("bar", result.Value);
}
[Fact]
public async void Should_Get_MultiDimensional_Array_Value()
{
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
var target = new ExpressionObserver(data, "Foo[1, 1]");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal("qux", result.Value);
}
[Fact]
public async void Should_Get_List_Value()
{
var data = new { Foo = new List<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal("bar", result.Value);
}
[Fact]
public void Should_Track_INCC_Add()
{
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Foo.Add("baz");
Assert.Equal(new[] { null, "baz" }, result);
}
[Fact]
public void Should_Track_INCC_Remove()
{
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[0]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Foo.RemoveAt(0);
Assert.Equal(new[] { "foo", "bar" }, result);
}
[Fact]
public void Should_Track_INCC_Replace()
{
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Foo[1] = "baz";
Assert.Equal(new[] { "bar", "baz" }, result);
}
[Fact]
public void Should_Track_INCC_Move()
{
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Foo.Move(0, 1);
Assert.Equal(new[] { "bar", "foo" }, result);
}
[Fact]
public void Should_Track_INCC_Reset()
{
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Foo.Clear();
Assert.Equal(new[] { "bar", null }, result);
}
}
}

97
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs

@ -0,0 +1,97 @@
// 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.Reactive.Linq;
using Perspex.Markup.Binding;
using Xunit;
namespace Perspex.Markup.UnitTests.Binding
{
public class ExpressionObserverTests_Negation
{
[Fact]
public async void Should_Negate_Boolean_Value()
{
var data = new { Foo = true };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal(false, result.Value);
}
[Fact]
public async void Should_Negate_0()
{
var data = new { Foo = 0 };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal(true, result.Value);
}
[Fact]
public async void Should_Negate_1()
{
var data = new { Foo = 1 };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal(false, result.Value);
}
[Fact]
public async void Should_Negate_False_String()
{
var data = new { Foo = "false" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal(true, result.Value);
}
[Fact]
public async void Should_Negate_True_String()
{
var data = new { Foo = "True" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal(false, result.Value);
}
[Fact]
public async void Should_Return_Empty_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.False(result.HasValue);
}
[Fact]
public async void Should_Return_Empty_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.False(result.HasValue);
}
[Fact]
public void SetValue_Should_Throw()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
Assert.Throws<NotSupportedException>(() => target.SetValue("bar"));
}
}
}

68
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs

@ -0,0 +1,68 @@
// 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.Reactive.Linq;
using System.Reactive.Subjects;
using Perspex.Markup.Binding;
using Xunit;
namespace Perspex.Markup.UnitTests.Binding
{
public class ExpressionObserverTests_Observable
{
[Fact]
public void Should_Get_Simple_Observable_Value()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
source.OnNext("bar");
sync.ExecutePostedCallbacks();
Assert.Equal(new[] { null, "foo", "bar" }, result);
}
}
[Fact]
public void Should_Get_Property_Value_From_Observable()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new Class1();
var target = new ExpressionObserver(data, "Next.Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Next.OnNext(new Class2("foo"));
sync.ExecutePostedCallbacks();
Assert.Equal(new[] { null, "foo" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
}
}
private class Class1 : NotifyingBase
{
public Subject<Class2> Next { get; } = new Subject<Class2>();
}
private class Class2 : NotifyingBase
{
public Class2(string foo)
{
Foo = foo;
}
public string Foo { get; }
}
}
}

242
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs

@ -0,0 +1,242 @@
// 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.Reactive.Linq;
using Perspex.Markup.Binding;
using Xunit;
namespace Perspex.Markup.UnitTests.Binding
{
public class ExpressionObserverTests_Property
{
[Fact]
public async void Should_Get_Simple_Property_Value()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal("foo", result.Value);
}
[Fact]
public async void Should_Get_Simple_Property_Chain()
{
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal("baz", result.Value);
}
[Fact]
public async void Should_Not_Have_Value_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.False(result.HasValue);
}
[Fact]
public void Should_Track_Simple_Property_Value()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Foo = "bar";
Assert.Equal(new[] { "foo", "bar" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
}
[Fact]
public void Should_Track_End_Of_Property_Chain_Changing()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
((Class2)data.Next).Bar = "baz";
Assert.Equal(new[] { "bar", "baz" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Next.SubscriptionCount);
}
[Fact]
public void Should_Track_Property_Chain_Changing()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
var old = data.Next;
data.Next = new Class2 { Bar = "baz" };
Assert.Equal(new[] { "bar", "baz" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Next.SubscriptionCount);
Assert.Equal(0, old.SubscriptionCount);
}
[Fact]
public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
var old = data.Next;
data.Next = null;
data.Next = new Class2 { Bar = "baz" };
Assert.Equal(new[] { "bar", null, "baz" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Next.SubscriptionCount);
Assert.Equal(0, old.SubscriptionCount);
}
[Fact]
public void Should_Track_Property_Chain_Breaking_With_Object_Then_Mending()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
var old = data.Next;
var breaking = new WithoutBar();
data.Next = breaking;
data.Next = new Class2 { Bar = "baz" };
Assert.Equal(new[] { "bar", null, "baz" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Next.SubscriptionCount);
Assert.Equal(0, breaking.SubscriptionCount);
Assert.Equal(0, old.SubscriptionCount);
}
[Fact]
public void SetValue_Should_Set_Simple_Property_Value()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
Assert.True(target.SetValue("bar"));
Assert.Equal("bar", data.Foo);
}
[Fact]
public void SetValue_Should_Set_Property_At_The_End_Of_Chain()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
Assert.True(target.SetValue("baz"));
Assert.Equal("baz", ((Class2)data.Next).Bar);
}
[Fact]
public void SetValue_Should_Return_False_For_Missing_Property()
{
var data = new Class1 { Next = new WithoutBar()};
var target = new ExpressionObserver(data, "Next.Bar");
Assert.False(target.SetValue("baz"));
}
[Fact]
public void SetValue_Should_Return_False_For_Missing_Object()
{
var data = new Class1();
var target = new ExpressionObserver(data, "Next.Bar");
Assert.False(target.SetValue("baz"));
}
[Fact]
public void SetValue_Should_Throw_For_Wrong_Type()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
Assert.Throws<ArgumentException>(() => target.SetValue(1.2));
}
private interface INext
{
int SubscriptionCount { get; }
}
private class Class1 : NotifyingBase
{
private string _foo;
private INext _next;
public string Foo
{
get { return _foo; }
set
{
_foo = value;
RaisePropertyChanged(nameof(Foo));
}
}
public INext Next
{
get { return _next; }
set
{
_next = value;
RaisePropertyChanged(nameof(Next));
}
}
}
private class Class2 : NotifyingBase, INext
{
private string _bar;
public string Bar
{
get { return _bar; }
set
{
_bar = value;
RaisePropertyChanged(nameof(Bar));
}
}
}
private class WithoutBar : NotifyingBase, INext
{
}
}
}

87
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs

@ -0,0 +1,87 @@
// 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.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Perspex.Markup.Binding;
using Xunit;
namespace Perspex.Markup.UnitTests.Binding
{
public class ExpressionObserverTests_Task
{
[Fact]
public void Should_Get_Simple_Task_Value()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
tcs.SetResult("foo");
sync.ExecutePostedCallbacks();
Assert.Equal(new object[] { null, "foo" }, result.ToArray());
}
}
[Fact]
public void Should_Get_Completed_Task_Value()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new { Foo = Task.FromResult("foo") };
var target = new ExpressionObserver(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
Assert.Equal(new object[] { "foo" }, result.ToArray());
}
}
[Fact]
public void Should_Get_Property_Value_From_Task()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var tcs = new TaskCompletionSource<Class2>();
var data = new Class1(tcs.Task);
var target = new ExpressionObserver(data, "Next.Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
tcs.SetResult(new Class2("foo"));
sync.ExecutePostedCallbacks();
Assert.Equal(new object[] { null, "foo" }, result.ToArray());
}
}
private class Class1 : NotifyingBase
{
public Class1(Task<Class2> next)
{
Next = next;
}
public Task<Class2> Next { get; }
}
private class Class2 : NotifyingBase
{
public Class2(string foo)
{
Foo = foo;
}
public string Foo { get; }
}
}
}

42
tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs

@ -0,0 +1,42 @@
// 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.ComponentModel;
using System.Linq;
namespace Perspex.Markup.UnitTests.Binding
{
public class NotifyingBase : INotifyPropertyChanged
{
private PropertyChangedEventHandler _propertyChanged;
public event PropertyChangedEventHandler PropertyChanged
{
add
{
_propertyChanged += value;
++SubscriptionCount;
}
remove
{
if (_propertyChanged?.GetInvocationList().Contains(value) == true)
{
_propertyChanged -= value;
--SubscriptionCount;
}
}
}
public int SubscriptionCount
{
get;
private set;
}
protected void RaisePropertyChanged(string propertyName)
{
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

113
tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props')" />
<Import Project="..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8EF392D5-1416-45AA-9956-7CBBC3229E8A}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Perspex.Markup.UnitTests</RootNamespace>
<AssemblyName>Perspex.Markup.UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="xunit.assert, Version=2.0.0.2929, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="xunit.core, Version=2.0.0.2929, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Binding\ExpressionNodeBuilderTests_Errors.cs" />
<Compile Include="Binding\ExpressionObserverTests_Observable.cs" />
<Compile Include="Binding\ExpressionObserverTests_Task.cs" />
<Compile Include="Binding\ExpressionObserverTests_Indexer.cs" />
<Compile Include="Binding\ExpressionObserverTests_Negation.cs" />
<Compile Include="Binding\ExpressionObserverTests_Property.cs" />
<Compile Include="Binding\ExpressionNodeBuilderTests.cs" />
<Compile Include="Binding\NotifyingBase.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UnitTestSynchronizationContext.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Perspex.Markup\Perspex.Markup.csproj">
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
<Name>Perspex.Markup</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props'))" />
<Error Condition="!Exists('..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

36
tests/Perspex.Markup.UnitTests/Properties/AssemblyInfo.cs

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Perspex.Markup.UnitTests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Perspex.Markup.UnitTests")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("8ef392d5-1416-45aa-9956-7cbbc3229e8a")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

68
tests/Perspex.Markup.UnitTests/UnitTestSynchronizationContext.cs

@ -0,0 +1,68 @@
// 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.Reactive.Disposables;
using System.Threading;
namespace Perspex.Markup.UnitTests
{
internal sealed class UnitTestSynchronizationContext : SynchronizationContext
{
readonly List<Tuple<SendOrPostCallback, object>> _postedCallbacks =
new List<Tuple<SendOrPostCallback, object>>();
public static Scope Begin()
{
var sync = new UnitTestSynchronizationContext();
var old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(sync);
return new Scope(old, sync);
}
public override void Send(SendOrPostCallback d, object state)
{
d(state);
}
public override void Post(SendOrPostCallback d, object state)
{
lock (_postedCallbacks)
{
_postedCallbacks.Add(Tuple.Create(d, state));
}
}
public void ExecutePostedCallbacks()
{
lock (_postedCallbacks)
{
_postedCallbacks.ForEach(t => t.Item1(t.Item2));
_postedCallbacks.Clear();
}
}
public class Scope : IDisposable
{
private SynchronizationContext _old;
private UnitTestSynchronizationContext _new;
public Scope(SynchronizationContext old, UnitTestSynchronizationContext n)
{
_old = old;
_new = n;
}
public void Dispose()
{
SynchronizationContext.SetSynchronizationContext(_old);
}
public void ExecutePostedCallbacks()
{
_new.ExecutePostedCallbacks();
}
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save