IScrollInfo in Avalon part II

At the end of the first part we have created a custom panel that implements IScrollInfo, but if you try to scroll we throw exceptions, and we cannot see the content. Since this is a Panel, we need to start seeing content so that we can scroll the content later.

First we will implement MeasureOverride. We will take the viewport size (which is given to us, because we have set CanContentScroll to True in the ScrollViewer) and measure our elements. As a consequence of measuring the elements, you will now know the total size that all of the elements take up in the AnnoyingPanel, and hence how big the total AnnoyingPanel area is. We call this area the Extent of the AnnoyingPanel. As you can see from the IScrollInfo interface, you are expected to write getters for the Extent, the Viewport and the Offset in each dimension.

In our AnnoyingPanel, the extent changes whenever MeasureOverride is called, because our child size is a function of viewport size. Child width is equal to viewport width, and child height is equal to double the viewport height. And the children are vertically stacked. Now we know all that we need to know about how to know what our extent and viewport are. We need to make fields to track these things, and update them in MeasureOverride, and let the ScrollOwner that they were modified. This code is below:

        private Size _extent = new Size(0, 0);

        private Size _viewport = new Size(0, 0);

        protected override Size MeasureOverride(Size availableSize)

        {

            Size childSize = new Size(

                availableSize.Width,

                (availableSize.Height * 2) / this.InternalChildren.Count);

            Size extent = new Size(

                availableSize.Width,

                childSize.Height * this.InternalChildren.Count);

            if (extent != _extent)

            {

                _extent = extent;

                if (_owner != null)

                    _owner.InvalidateScrollInfo();

       }

            if (availableSize != _viewport)

            {

                _viewport = availableSize;

                if (_owner != null)

                    _owner.InvalidateScrollInfo();

            }

            foreach (UIElement child in this.InternalChildren)

            {

                child.Measure(childSize);

            }

            return availableSize;

        }

Try to compile and run this - you should see an exception in get_HorizontalOffset. We have made some progress, because we made the ScrollOwner more curious about us and now it wants to know more. Since we updated the extent, it wants to see what the position is in the scroll area (called the offset). We need to make a field to store this, and we need to implement all of those property getters from IScrollInfo now, because the ScrollOwner will start asking for all of them:

        private Point _offset;

        public double HorizontalOffset

        {

            get { return _offset.X; }

        }

        public double VerticalOffset

        {

            get { return _offset.Y; }

        }

       public double ExtentHeight

        {

            get { return _extent.Height; }

        }

        public double ExtentWidth

        {

            get { return _extent.Width; }

        }

        public double ViewportHeight

        {

            get { return _viewport.Height; }

        }

        public double ViewportWidth

        {

            get { return _viewport.Width; }

        }

Now compile and run again - no exceptions, and still no children! That is because we have not arranged the children yet. Time to implement ArrangeOverride so that we do have this children arranging code. Most of the logic for ArrangeOverride is the same as for the measure case in this panel (and custom panels tend to use caching and other smarts to not repeat a lot of this work. But this is a scrolling tutorial, and not a layout tutorial, so we will do enough to make it work and move on). The only difference we have is that we will call Arrange on our children and set their positions.

        protected override Size ArrangeOverride(Size finalSize)

        {

            Size childSize = new Size(

                finalSize.Width,

                (finalSize.Height * 2) / this.InternalChildren.Count);

            Size extent = new Size(

                finalSize.Width,

                childSize.Height * this.InternalChildren.Count);

            if (extent != _extent)

            {

                _extent = extent;

           if (_owner != null)

                    _owner.InvalidateScrollInfo();

            }

            if (finalSize != _viewport)

            {

                _viewport = finalSize;

                if (_owner != null)

                    _owner.InvalidateScrollInfo();

            }

            for (int i = 0; i < this.InternalChildren.Count; i++)

            {

                this.InternalChildren[i].Arrange(new Rect(0, childSize.Height * i, childSize.Width, childSize.Height));

            }

           return finalSize;

        }

Now compile and run the app, and you should see a window with a button in it - and the button will have the text clipped across the bottom, because you are only seeing the top half. And there should be a scrollbar on the side, with the thumb half the size of the tracker. We have an AnnoyingPanel doing some custom layout!

If you try to do anything with the scrollbar, then you will get an exception, because you have not implemented any of the scroll command methods such as LineUp or SetVerticalOffset. Tune in to part III to read all about how to implement these commands.