IScrollInfo tutorial part IV

After our last installment, which was written many moons ago (literally), the only remaining thing left to do (apart from clean up the code a bit) was to implement the IScrollInfo.MakeVisible method. Somebody has requested that I address this, so I am going to attempt to fulfill that.

The idea of MakeVisible is that it is called to ensure the visiblity of one of the children in the Panel. When will this be called? Well, if you fire up the AnnoyingPanel sample that has been written in the past three chapters, and click one of the buttons, you will notice that the application will crash because of the exception we are firing from MakeVisible. Pushing the tab key will also have this result. This makes sense - when you push a button or tab to it, WPF will ensure that the button is visible so that you can see what you have tabbed to or clicked on.

So how do we implement the method? The method takes a Visual and a Rect. The Visual is the thing that we need to make visible - it will be one of the children of the Panel. The Rect is the rectangle of the Visual that needs to be visible. The return value, another Rect, is the rectangle of the Visual that you managed to make visible.

So to implement this method for our sample should be fairly simple. We will search our internal child list for the Visual to see if it is there. Based on where it is in the child list, and the size of the children, we can calculate the offset that we need to scroll to in order to have the child at the top of the viewport. Then we can set the new vertical offset, and return the rectangle we were given, since we know that the children are always smaller than the viewport.

        public Rect MakeVisible(Visual visual, Rect rectangle)

        {

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

            {

                if ((Visual)this.InternalChildren[i] == visual)

                {

                    // we found the visual! Let's scroll it into view. First we need to know how big

                    // each child is.

                    Size finalSize = this.RenderSize;

                    Size childSize = new Size(

                        finalSize.Width,

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

                    // now we can calculate the vertical offset that we need and set it

                    SetVerticalOffset(childSize.Height * i);

                    // child size is always smaller than viewport, because that is what makes the Panel

                    // an AnnoyingPanel.

                    return rectangle;

                }

            }

            throw new ArgumentException("Given visual is not in this Panel");

        }

If you build this and run it then things will look the same, but now when you click or tab to a button it will be forced to the top of the viewport (or as close as it can get to there, anyway). So now we have a minimal working version of MakeVisible.

There are several issues with our implementation, and they are things to think about if you are implementing this for real in a real world application:

  • Our implementation is repeating the calculation of child size again. We have this code littered throughout the sample in many places. I have done this to try and make it easier to grok what is going on, but this stuff should really be in helpers to avoid bugs from having it being done slightly differently in places.
  • We are assuming that the only difference in the coordinate system of the Visual and the Panel is the position. WPF allows much more interesting transforms, and I am unsure at this time what the input and output should look like. This is an interesting experiment for somebody to try one day.
  • Our calculation of the rectangle that we scrolled into view is simple, because we always try to move the child to the top of the viewport, and the children are always smaller than the viewport. This shortcut might not apply depending on how your Panel works, and you may need to do some calculation to figure out how much you are showing.
  • We are always changing the vertical offset, even if the child is already visible. This is a distracting way to implement things - the user does not expect the child to move around when it is already visible. A real implementation would look at the offset, and decide if any scrolling is needed or not. If scrolling is not needed, but the child is partially shown, then the returned rectangle would need to change as well.

All of the four things above are incremental, and typically change depending on your scenario, which is why I am not being too specific about what to do or demonstrating it. IScrollInfo is an extension point designed to allow a maximum of flexibility, and there is a huge number of ways that you can use it. All that I am trying to do is let you be aware of the possibilities.

I have attached the final AnnoyingPanel.cs file for you to look at if you want all of the code. You may need to change the namespace to use it in your app, but I figure that this small implementation might be a starting point that somebody would want to use.

AnnoyingPanel.cs

Comments

  • Anonymous
    December 18, 2006
    Hi,This is a great series.  I really enjoyed going through it.How would I apply this to controls (such as ListView) that maintain thier own ScrollViewer?  Can I intercept the scrollViewer's events?  ListView doesn't expose events for LineDown, LineUp, etc.ThanksHouman
  • Anonymous
    January 14, 2007
    I think this series is to help people implement their own controls that support IScrollInfo.
  • Anonymous
    September 28, 2007
    how come you don't link to previous post.
  • Anonymous
    October 07, 2007
    I have one Problem with ScrollViewer, i.e 1.If Iam using a ScrollViewer inside a treeview say  t1 , if I  focus on scrollviewer and scroll the content , t1 content is scrolling fine,
  1. if I  scroll the content inside t1  , t1 content is scrolling fine. But the  Problem Is : If I place the scrollviewer outside  of the treeview t1 , and if I scroll the content keeping the focus on the selected treeviewitem , Now the Content is not scrolling ??? I need your assistance in solving this problem ...
  • Anonymous
    October 07, 2007
    This is very helpful series ....

  • Anonymous
    December 20, 2007
    I'll add my kudos as well.  I'm working on a control that will require some custom scrolling across a host of subcontrols working in tandem, and this was a fantastic starting point. I'd also recommend looking at http://www.wiredprairie.us/journal/2007/04/photoscroll_the_worst_named_wp.html for an example of an app that implements a custom scroll bar.  It contains some useful insights as well. Thanks!! David Cater

  • Anonymous
    July 24, 2008
    Ben, thank you for this article, it is very helpful.But your code contains a bug. Please replace the SetVerticalOffset method to following:public void SetVerticalOffset(double offset){ _offset.Y = Math.Max(0, Math.Min(_extent.Height - _viewport.Height, Math.Max(0, offset))); if (_owner != null) _owner.InvalidateScrollInfo(); _trans.Y = -_offset.Y;}

  • Anonymous
    July 24, 2008
    Oops! Correction for my previous feedback:public void SetVerticalOffset(double offset){ _offset.Y = Math.Max(0, Math.Min(_extent.Height - _viewport.Height, Math.Max(0, offset))); if (_owner != null) _owner.InvalidateScrollInfo(); _trans.Y = -_offset.Y; InvalidateMeasure();}