IScrollInfo in Avalon part III

When we last left the application it had the appearance of something that could scroll, but exceptions were being thrown from unimplemented members left and right. This posting will help out with that.

Let's look at the SetVerticalOffset method as the first case. This method is called when the user drags the thumb on the vertical scrollbar. You are given an offset position, and you do the work to update the scroll position and scroll trhe content. Updating the scroll position is straightforward, but if all you do is change the value of the offset, then the content will not scroll. Because we are implementing IScrollInfo, and we set CanContentScroll, then we are expected to do the work of moving the content around. One way to do this is to use a render transform - this gives us an easy way to offset the rendered position of the content. First we create a render transform and set it on our panel:

        private TranslateTransform _trans = new TranslateTransform();

        public AnnoyingPanel()

        {

            this.RenderTransform = _trans;

        }

This gives us a transform that we can use later on. Now we have enough pieces to implement SetVerticalOffset:

        public void SetVerticalOffset(double offset)

        {

            if (offset < 0 || _viewport.Height >= _extent.Height)

            {

                offset = 0;

            }

            else

            {

                if (offset + _viewport.Height >= _extent.Height)

                {

                    offset = _extent.Height - _viewport.Height;

                }

            }

            _offset.Y = offset;

            if (_owner != null)

                _owner.InvalidateScrollInfo();

   

            _trans.Y = -offset;

        }

Why do we have all of the checking at the start? Won't Avalon only give us valid offsets? Besides the fact that input validation is pretty much always a good idea, we are going to use this method ourselves to help implement other scrolling methods later, and this gives us a centralized place to check the offsets. What checking do we do? First we check if the given offset is less than zero, or if the viewport is larger than the extent. In both of these cases, we need to set the offset to zero. A negative offset makes no sense, and if the viewport is larger than the extent then there is nowhere to scroll and we can set the offset to zero as well.

The other case we need to check is to see if the offset goes too far. The maximum value of the offset that makes sense is the extent minus the viewport height. To see why, imagine the case where the scroll offset is at the maximum allowed value. The bottom of our content will sit at the bottom of the viewport. But the offset is measuring the top of the viewport - so we need to subtract the viewport height from the extent height to get the maximum position.

Compile the app now and you should be able to drag the scroll thumb and scroll your content. Isn't that cool? You can see that we must be really close to the end now. All you need to do is implement the scroll command methods to stop the rest of the exceptions from scroll bar interactions. First we have the LineUp and LineDown methods - we will implement these by changing the scroll position by 1. These methods are called when the scroll up/down buttons are clicked:

        public void LineUp()

        {

            SetVerticalOffset(this.VerticalOffset - 1);

        }

        public void LineDown()

        {

            SetVerticalOffset(this.VerticalOffset + 1);

        }

Now we see why we have the checking before - adding 1 or subtracting 1 will not always give us a valid scroll offset. If you compile now, then the up and down buttons will work as well. How about PageUp/PageDown? Because you are implementing IScrollInfo then you can choose what a page means. For our panel, we will make a full control equal to a page. Since we know that content in our panel is twice the viewport height, we can use this to implement the page methods:

        public void PageUp()

        {

            double childHeight = (_viewport.Height * 2) / this.InternalChildren.Count;

            SetVerticalOffset(this.VerticalOffset - childHeight);

        }

        public void PageDown()

        {

            double childHeight = (_viewport.Height * 2) / this.InternalChildren.Count;

            SetVerticalOffset(this.VerticalOffset + childHeight);

        }

Again, there is scope to refactor this code so that you can have less repetition. A member to calculate the child size would be useful. The last methods that we need for vertical scrolling are the mouse wheel methods. We can implement them by moving in steps of 10:

        public void MouseWheelUp()

        {

            SetVerticalOffset(this.VerticalOffset - 10);

        }

        public void MouseWheelDown()

        {

            SetVerticalOffset(this.VerticalOffset + 10);

        }

And now all interactions with the vertical scrollbar are working, including mouse wheel support. Does this conclude the tutorial? Not quite - we still have not implemented MakeVisible, and we have some subtle bugs with the current implementation that you will see depending on your scroll position when you resize the window. This will be explained in detail in the next installment.