diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs
new file mode 100644
index 0000000000..9a0bc8de73
--- /dev/null
+++ b/samples/XamlTestApplicationPcl/TestScrollable.cs
@@ -0,0 +1,92 @@
+using System;
+using Perspex;
+using Perspex.Controls;
+using Perspex.Controls.Primitives;
+using Perspex.Media;
+
+namespace XamlTestApplication
+{
+ public class TestScrollable : Control, IScrollable
+ {
+ private int itemCount = 100;
+ private Action _invalidateScroll;
+ private Size _extent;
+ private Vector _offset;
+ private Size _viewport;
+ private Size _lineSize;
+
+ public Action InvalidateScroll
+ {
+ get { return _invalidateScroll; }
+ set { _invalidateScroll = value; }
+ }
+
+ Size IScrollable.Extent
+ {
+ get { return _extent; }
+ }
+
+ Vector IScrollable.Offset
+ {
+ get
+ {
+ return _offset;
+ }
+
+ set
+ {
+ _offset = value;
+ InvalidateVisual();
+ }
+ }
+
+ Size IScrollable.Viewport
+ {
+ get { return _viewport; }
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ using (var line = new FormattedText(
+ "Item 100",
+ TextBlock.GetFontFamily(this),
+ TextBlock.GetFontSize(this),
+ TextBlock.GetFontStyle(this),
+ TextAlignment.Left,
+ TextBlock.GetFontWeight(this)))
+ {
+ line.Constraint = availableSize;
+ _lineSize = line.Measure();
+ return new Size(_lineSize.Width, _lineSize.Height * itemCount);
+ }
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ _viewport = new Size(finalSize.Width, finalSize.Height / _lineSize.Height);
+ _extent = new Size(_lineSize.Width, itemCount);
+ InvalidateScroll();
+ return finalSize;
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ var y = 0.0;
+
+ for (var i = (int)_offset.Y; i < itemCount; ++i)
+ {
+ using (var line = new FormattedText(
+ "Item " + (i + 1),
+ TextBlock.GetFontFamily(this),
+ TextBlock.GetFontSize(this),
+ TextBlock.GetFontStyle(this),
+ TextAlignment.Left,
+ TextBlock.GetFontWeight(this)))
+ {
+ context.DrawText(Brushes.Black, new Point(-_offset.X, y), line);
+ y += _lineSize.Height;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml
index c19220deb7..12440b4106 100644
--- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml
+++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml
@@ -1,6 +1,7 @@
@@ -272,6 +273,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj
index 46358e5836..a32ae5eec6 100644
--- a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj
+++ b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj
@@ -41,6 +41,7 @@
+
diff --git a/src/Perspex.Controls/ContentControl.cs b/src/Perspex.Controls/ContentControl.cs
index 2899b58bcd..e8b52a2948 100644
--- a/src/Perspex.Controls/ContentControl.cs
+++ b/src/Perspex.Controls/ContentControl.cs
@@ -36,8 +36,6 @@ namespace Perspex.Controls
public static readonly PerspexProperty VerticalContentAlignmentProperty =
PerspexProperty.Register(nameof(VerticalContentAlignment));
- private IDisposable _presenterSubscription;
-
///
/// Initializes static members of the class.
///
diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj
index b85fe8461b..6bd1dd9162 100644
--- a/src/Perspex.Controls/Perspex.Controls.csproj
+++ b/src/Perspex.Controls/Perspex.Controls.csproj
@@ -56,6 +56,7 @@
+
@@ -81,9 +82,7 @@
-
-
diff --git a/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs b/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs
index 65aa4ae4f9..1ea8b3ef41 100644
--- a/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs
@@ -3,6 +3,9 @@
using System;
using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Perspex.Controls.Primitives;
using Perspex.Input;
using Perspex.Layout;
using Perspex.VisualTree;
@@ -39,6 +42,7 @@ namespace Perspex.Controls.Presenters
PerspexProperty.Register("CanScrollHorizontally", true);
private Size _measuredExtent;
+ private IDisposable _scrollableSubscription;
///
/// Initializes static members of the class.
@@ -56,6 +60,8 @@ namespace Perspex.Controls.Presenters
public ScrollContentPresenter()
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
+
+ GetObservable(ChildProperty).Subscribe(ChildChanged);
}
///
@@ -149,19 +155,24 @@ namespace Perspex.Controls.Presenters
///
protected override Size MeasureOverride(Size availableSize)
{
- var content = Content as ILayoutable;
+ var child = Child;
- if (content != null)
+ if (child != null)
{
- var measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
+ var measureSize = availableSize;
- if (!CanScrollHorizontally)
+ if (_scrollableSubscription == null)
{
- measureSize = measureSize.WithWidth(availableSize.Width);
+ measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
+
+ if (!CanScrollHorizontally)
+ {
+ measureSize = measureSize.WithWidth(availableSize.Width);
+ }
}
- content.Measure(measureSize);
- var size = content.DesiredSize;
+ child.Measure(measureSize);
+ var size = child.DesiredSize;
_measuredExtent = size;
return size.Constrain(availableSize);
}
@@ -175,16 +186,21 @@ namespace Perspex.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize)
{
var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable;
+ var offset = default(Vector);
- Viewport = finalSize;
- Extent = _measuredExtent;
+ if (_scrollableSubscription == null)
+ {
+ Viewport = finalSize;
+ Extent = _measuredExtent;
+ offset = Offset;
+ }
if (child != null)
{
var size = new Size(
Math.Max(finalSize.Width, child.DesiredSize.Width),
Math.Max(finalSize.Height, child.DesiredSize.Height));
- child.Arrange(new Rect((Point)(-Offset), size));
+ child.Arrange(new Rect((Point)(-offset), size));
return finalSize;
}
@@ -209,6 +225,30 @@ namespace Perspex.Controls.Presenters
e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect);
}
+ private void ChildChanged(IControl child)
+ {
+ var scrollable = child as IScrollable;
+
+ _scrollableSubscription?.Dispose();
+ _scrollableSubscription = null;
+
+ if (scrollable != null)
+ {
+ scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable);
+ _scrollableSubscription = new CompositeDisposable(
+ GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x),
+ Disposable.Create(() => scrollable.InvalidateScroll = null));
+ UpdateFromScrollable(scrollable);
+ }
+ }
+
+ private void UpdateFromScrollable(IScrollable scrollable)
+ {
+ Viewport = scrollable.Viewport;
+ Extent = scrollable.Extent;
+ Offset = scrollable.Offset;
+ }
+
private static Vector ValidateOffset(ScrollContentPresenter o, Vector value)
{
return ScrollViewer.CoerceOffset(
diff --git a/src/Perspex.Controls/Primitives/IScrollInfo.cs b/src/Perspex.Controls/Primitives/IScrollInfo.cs
deleted file mode 100644
index 59dbc085e6..0000000000
--- a/src/Perspex.Controls/Primitives/IScrollInfo.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-// 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.Text;
-using System.Threading.Tasks;
-
-namespace Perspex.Controls.Primitives
-{
- public interface IScrollInfoBase
- {
- ///
- /// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependant
- /// on this IScrollInfo's properties. Implementers of IScrollInfo should call InvalidateScrollInfo()
- /// on this object when properties change.
- ///
- ScrollViewer ScrollOwner { get; set; }
-
- Rect MakeVisible(Visual visual, Rect rectangle);
- }
-
- public interface IVerticalScrollInfo : IScrollInfoBase
- {
- ///
- /// VerticalOffset is the vertical offset into the scrolled content that represents the first unit visible.
- ///
- double VerticalOffset { get; set; }
-
- ///
- /// ExtentHeight contains the full vertical range of the scrolled content.
- ///
- double ExtentHeight { get; }
-
- ///
- /// ViewportHeight contains the currently visible vertical range of the scrolled content.
- ///
- double ViewportHeight { get; }
-
- ///
- /// This property indicates to the IScrollInfo whether or not it can scroll in the vertical given dimension.
- ///
- bool CanVerticallyScroll { get; set; }
-
- void LineDown();
-
- void LineUp();
-
- void MouseWheelDown();
-
- void MouseWheelUp();
-
- void PageDown();
-
- void PageUp();
- }
-
- public interface IHorizontalScrollInfo : IScrollInfoBase
- {
- ///
- /// ExtentWidth contains the full horizontal range of the scrolled content.
- ///
- double ExtentWidth { get; }
-
- ///
- /// ViewportWidth contains the currently visible horizontal range of the scrolled content.
- ///
- double ViewportWidth { get; }
-
- ///
- /// HorizontalOffset is the horizontal offset into the scrolled content that represents the first unit visible.
- ///
- double HorizontalOffset { get; set; }
-
- ///
- /// This property indicates to the IScrollInfo whether or not it can scroll in the horizontal given dimension.
- ///
- bool CanHorizontallyScroll { get; set; }
-
- void LineLeft();
-
- void LineRight();
-
- void MouseWheelLeft();
-
- void MouseWheelRight();
-
- void PageLeft();
-
- void PageRight();
- }
-
- public interface IScrollInfo : IHorizontalScrollInfo, IVerticalScrollInfo
- {
- }
-}
diff --git a/src/Perspex.Controls/Primitives/IScrollable.cs b/src/Perspex.Controls/Primitives/IScrollable.cs
new file mode 100644
index 0000000000..65adadb15e
--- /dev/null
+++ b/src/Perspex.Controls/Primitives/IScrollable.cs
@@ -0,0 +1,39 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Perspex.Controls.Primitives
+{
+ ///
+ /// Interface implemented by controls that handle their own scrolling when placed inside a
+ /// .
+ ///
+ public interface IScrollable
+ {
+ ///
+ /// Gets or sets the invalidation method which notifies the attached
+ /// of a change in or .
+ ///
+ ///
+ /// This property is set by the parent when the
+ /// is placed inside it.
+ ///
+ Action InvalidateScroll { get; set; }
+
+ ///
+ /// Gets the extent of the scrollable content, in logical units
+ ///
+ Size Extent { get; }
+
+ ///
+ /// Gets or sets the current scroll offset, in logical units.
+ ///
+ Vector Offset { get; set; }
+
+ ///
+ /// Gets the size of the viewport, in logical units.
+ ///
+ Size Viewport { get; }
+ }
+}
diff --git a/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs b/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs
deleted file mode 100644
index 8ea94f6fb6..0000000000
--- a/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs
+++ /dev/null
@@ -1,126 +0,0 @@
-// 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.Controls.Primitives
-{
- public class ScrollInfoAdapter : IScrollInfo
- {
- private readonly IScrollInfoBase _nfo;
- public ScrollInfoAdapter(IScrollInfoBase nfo)
- {
- _nfo = nfo;
- }
-
- public ScrollViewer ScrollOwner
- {
- get { return _nfo.ScrollOwner; }
- set { _nfo.ScrollOwner = value; }
- }
-
- public double ExtentWidth => (_nfo as IHorizontalScrollInfo)?.ExtentWidth ?? 0;
-
- public double ViewportWidth => (_nfo as IHorizontalScrollInfo)?.ViewportWidth ?? 0;
-
- public double ExtentHeight => (_nfo as IVerticalScrollInfo)?.ExtentHeight ?? 0;
-
- public double ViewportHeight => (_nfo as IVerticalScrollInfo)?.ViewportHeight ?? 0;
-
- private double _horizontalOffset;
- public double HorizontalOffset
- {
- get
- {
- return (_nfo as IHorizontalScrollInfo)?.HorizontalOffset ?? _horizontalOffset;
- }
-
- set
- {
- var info = (_nfo as IHorizontalScrollInfo);
- if (info == null)
- _horizontalOffset = value;
- else
- info.HorizontalOffset = value;
- }
- }
-
- private double _verticalOffset;
- public double VerticalOffset
- {
- get
- {
- return (_nfo as IVerticalScrollInfo)?.VerticalOffset ?? _verticalOffset;
- }
-
- set
- {
- var info = (_nfo as IVerticalScrollInfo);
- if (info == null)
- _verticalOffset = value;
- else
- info.VerticalOffset = value;
- }
- }
-
- public void LineLeft() => (_nfo as IHorizontalScrollInfo)?.LineLeft();
-
- public void LineRight() => (_nfo as IHorizontalScrollInfo)?.LineRight();
-
- public void MouseWheelLeft() => (_nfo as IHorizontalScrollInfo)?.MouseWheelLeft();
-
- public void MouseWheelRight() => (_nfo as IHorizontalScrollInfo)?.MouseWheelRight();
-
- public void PageLeft() => (_nfo as IHorizontalScrollInfo)?.PageLeft();
-
- public Rect MakeVisible(Visual visual, Rect rectangle) => _nfo.MakeVisible(visual, rectangle);
-
- public void PageRight() => (_nfo as IHorizontalScrollInfo)?.PageRight();
-
- public void LineDown() => (_nfo as IVerticalScrollInfo)?.LineDown();
-
- public void LineUp() => (_nfo as IVerticalScrollInfo)?.LineUp();
-
- public void MouseWheelDown() => (_nfo as IVerticalScrollInfo)?.MouseWheelDown();
-
- public void MouseWheelUp() => (_nfo as IVerticalScrollInfo)?.MouseWheelUp();
-
- public void PageDown() => (_nfo as IVerticalScrollInfo)?.PageDown();
-
- public void PageUp() => (_nfo as IVerticalScrollInfo)?.PageUp();
-
- private bool _canVerticallyScroll;
- public bool CanVerticallyScroll
- {
- get
- {
- return (_nfo as IVerticalScrollInfo)?.CanVerticallyScroll ?? _canVerticallyScroll;
- }
-
- set
- {
- var info = (_nfo as IVerticalScrollInfo);
- if (info == null)
- _canVerticallyScroll = value;
- else
- info.CanVerticallyScroll = value;
- }
- }
-
- private bool _canHorizontallyScroll;
- public bool CanHorizontallyScroll
- {
- get
- {
- return (_nfo as IHorizontalScrollInfo)?.CanHorizontallyScroll ?? _canHorizontallyScroll;
- }
-
- set
- {
- var info = (_nfo as IHorizontalScrollInfo);
- if (info == null)
- _canHorizontallyScroll = value;
- else
- info.CanHorizontallyScroll = value;
- }
- }
- }
-}
diff --git a/src/Perspex.Controls/ScrollViewer.cs b/src/Perspex.Controls/ScrollViewer.cs
index 5af724a614..b454bc6d5e 100644
--- a/src/Perspex.Controls/ScrollViewer.cs
+++ b/src/Perspex.Controls/ScrollViewer.cs
@@ -2,6 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Reactive.Disposables;
using System.Reactive.Linq;
using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives;
@@ -117,6 +120,8 @@ namespace Perspex.Controls
nameof(VerticalScrollBarVisibility),
ScrollBarVisibility.Auto);
+ private IDisposable _scrollableSubscription;
+
///
/// Initializes static members of the class.
///
diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
index 19078b5b2e..2ec1569e11 100644
--- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
+++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
@@ -93,6 +93,7 @@
+
diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
index eb913b3410..adb4bfd0fa 100644
--- a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
+++ b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
@@ -142,6 +142,35 @@ namespace Perspex.Controls.UnitTests.Presenters
Assert.Equal(new Rect(-25, -25, 150, 150), content.Bounds);
}
+ [Fact]
+ public void Measure_Should_Pass_Bounded_X_If_CannotScrollHorizontally()
+ {
+ var child = new TestControl();
+ var target = new ScrollContentPresenter
+ {
+ Content = child,
+ [ScrollContentPresenter.CanScrollHorizontallyProperty] = false,
+ };
+
+ target.Measure(new Size(100, 100));
+
+ Assert.Equal(new Size(100, double.PositiveInfinity), child.AvailableSize);
+ }
+
+ [Fact]
+ public void Measure_Should_Pass_Unbounded_X_If_CanScrollHorizontally()
+ {
+ var child = new TestControl();
+ var target = new ScrollContentPresenter
+ {
+ Content = child,
+ };
+
+ target.Measure(new Size(100, 100));
+
+ Assert.Equal(Size.Infinity, child.AvailableSize);
+ }
+
[Fact]
public void Arrange_Should_Set_Viewport_And_Extent_In_That_Order()
{
@@ -240,8 +269,11 @@ namespace Perspex.Controls.UnitTests.Presenters
private class TestControl : Control
{
+ public Size AvailableSize { get; private set; }
+
protected override Size MeasureOverride(Size availableSize)
{
+ AvailableSize = availableSize;
return new Size(150, 150);
}
}
diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs
new file mode 100644
index 0000000000..0dc1c31462
--- /dev/null
+++ b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs
@@ -0,0 +1,216 @@
+// 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.Controls.Presenters;
+using Perspex.Controls.Primitives;
+using Xunit;
+
+namespace Perspex.Controls.UnitTests
+{
+ public class ScrollContentPresenterTests_IScrollable
+ {
+ [Fact]
+ public void Measure_Should_Pass_Unchanged_Bounds_To_IScrollable()
+ {
+ var scrollable = new TestScrollable();
+ var target = new ScrollContentPresenter
+ {
+ Content = scrollable,
+ };
+
+ target.Measure(new Size(100, 100));
+
+ Assert.Equal(new Size(100, 100), scrollable.AvailableSize);
+ }
+
+ [Fact]
+ public void Arrange_Should_Not_Offset_IScrollable_Bounds()
+ {
+ var scrollable = new TestScrollable
+ {
+ Extent = new Size(100, 100),
+ Offset = new Vector(50, 50),
+ Viewport = new Size(25, 25),
+ };
+
+ var target = new ScrollContentPresenter
+ {
+ Content = scrollable,
+ };
+
+ target.Measure(new Size(100, 100));
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds);
+ }
+
+ [Fact]
+ public void Arrange_Should_Not_Set_Viewport_And_Extent_With_IScrollable()
+ {
+ var target = new ScrollContentPresenter
+ {
+ Content = new TestScrollable()
+ };
+
+ var changed = false;
+
+ target.Measure(new Size(100, 100));
+
+ target.GetObservable(ScrollViewer.ViewportProperty).Skip(1).Subscribe(_ => changed = true);
+ target.GetObservable(ScrollViewer.ExtentProperty).Skip(1).Subscribe(_ => changed = true);
+
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ Assert.False(changed);
+ }
+
+ [Fact]
+ public void InvalidateScroll_Should_Be_Set_When_Set_As_Content()
+ {
+ var scrollable = new TestScrollable();
+ var target = new ScrollContentPresenter
+ {
+ Content = scrollable
+ };
+
+ target.ApplyTemplate();
+
+ Assert.NotNull(scrollable.InvalidateScroll);
+ }
+
+ [Fact]
+ public void InvalidateScroll_Should_Be_Cleared_When_Removed_From_Content()
+ {
+ var scrollable = new TestScrollable();
+ var target = new ScrollContentPresenter
+ {
+ Content = scrollable
+ };
+
+ target.ApplyTemplate();
+ target.Content = null;
+ target.ApplyTemplate();
+
+ Assert.Null(scrollable.InvalidateScroll);
+ }
+
+ [Fact]
+ public void Extent_Offset_And_Viewport_Should_Be_Read_From_IScrollable()
+ {
+ var scrollable = new TestScrollable
+ {
+ Extent = new Size(100, 100),
+ Offset = new Vector(50, 50),
+ Viewport = new Size(25, 25),
+ };
+
+ var target = new ScrollContentPresenter
+ {
+ Content = scrollable
+ };
+
+ target.ApplyTemplate();
+
+ Assert.Equal(scrollable.Extent, target.Extent);
+ Assert.Equal(scrollable.Offset, target.Offset);
+ Assert.Equal(scrollable.Viewport, target.Viewport);
+
+ scrollable.Extent = new Size(200, 200);
+ scrollable.Offset = new Vector(100, 100);
+ scrollable.Viewport = new Size(50, 50);
+
+ Assert.Equal(scrollable.Extent, target.Extent);
+ Assert.Equal(scrollable.Offset, target.Offset);
+ Assert.Equal(scrollable.Viewport, target.Viewport);
+ }
+
+ [Fact]
+ public void Offset_Should_Be_Written_To_IScrollable()
+ {
+ var scrollable = new TestScrollable
+ {
+ Extent = new Size(100, 100),
+ Offset = new Vector(50, 50),
+ };
+
+ var target = new ScrollContentPresenter
+ {
+ Content = scrollable
+ };
+
+ target.ApplyTemplate();
+
+ target.Offset = new Vector(25, 25);
+
+ Assert.Equal(target.Offset, scrollable.Offset);
+ }
+
+ [Fact]
+ public void Offset_Should_Not_Be_Written_To_IScrollable_After_Removal()
+ {
+ var scrollable = new TestScrollable
+ {
+ Extent = new Size(100, 100),
+ Offset = new Vector(50, 50),
+ };
+
+ var target = new ScrollContentPresenter
+ {
+ Content = scrollable
+ };
+
+ target.Content = null;
+ target.Offset = new Vector(25, 25);
+
+ Assert.Equal(new Vector(50, 50), scrollable.Offset);
+ }
+
+ private class TestScrollable : Control, IScrollable
+ {
+ private Size _extent;
+ private Vector _offset;
+ private Size _viewport;
+
+ public Size AvailableSize { get; private set; }
+ public Action InvalidateScroll { get; set; }
+
+ public Size Extent
+ {
+ get { return _extent; }
+ set
+ {
+ _extent = value;
+ InvalidateScroll?.Invoke();
+ }
+ }
+
+ public Vector Offset
+ {
+ get { return _offset; }
+ set
+ {
+ _offset = value;
+ InvalidateScroll?.Invoke();
+ }
+ }
+
+ public Size Viewport
+ {
+ get { return _viewport; }
+ set
+ {
+ _viewport = value;
+ InvalidateScroll?.Invoke();
+ }
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ AvailableSize = availableSize;
+ return new Size(150, 150);
+ }
+ }
+ }
+}
\ No newline at end of file