IScrollInfo in Avalon part I

Recently I invested the time to learn how to implement the IScrollInfo interface on a control in Avalon. This was an interesting learning experience, and since the existing documentation for this feature is not out in the wild yet, and I figure that people want to learn how to do this, I decided to write up a multi part post on how to do this.

Why would you implement this interface? If you a have a scenario with your own layout panel inside of a ScrollViewer, and you want to have more control over the layout and scrolling, then the best way to achieve this is to set CanContentScroll to True on your ScrollViewer and implement IScrollInfo on the control inside of the ScrollViewer. The two main benefits that you will gain from this are:

  • The Measure and Arrange for your scrolling content will now be passed down the viewport size from the ScrollViewer (which is the area that is visible to the user inside of the ScrollViewer) rather than being passed a size with one or more infinite extents.
  • The ScrollViewer will delegate the work of keeping track of the scrolling information (the scroll position, the extent of the area being scrolled) and call your methods to update this information (for instance, you get called when the thumb is moved or the scroll buttons are clicked).

Why don't people always implement the interface? This is a large interface (15 methods and 9 properties by my count in the December CTP build of Avalon) and there is no good guide on how to start implementing the interface. But hopefully this series of posts will help to rectify that.

To get started, make an Avalon project in whatever way you normally do. Then add a new class to your project, and call it AnnoyingPanel. We will implement an annoying panel that stacks of its children vertically, and sizes the children so that they are always twice the height of the viewport. This will demonstrate making use of the scroll viewport size, and demonstrate how to keep track of the scrolling information and implement the scrolling methods. [update] You should derive AnnoyingPanel from Panel, and to make it a public class.

The first step is to add the IScrollInfo interface to AnnoyingPanel. If you have VS 2005 installed, this is very quick, because you just add the interface to the class, and right click on the interface text and select "Implement interface". This will stub out all of the methods that you need. They will all throw exceptions, which is very handy. The order in which we will implement things is the order in which we need them (like how movies sometimes do the credits in the order of appearance). [update] If you cannot find IScrollInfo, it is in the System.Windows.Controls.Primitives namespace.

Now add the new panel to the XAML for the window. Here is what mine looks like (you need to make sure that your namespace and class names match):

<?Mapping XmlNamespace="local" ClrNamespace="AnnoyingPanelSample" ?>

<Window x:Class="AnnoyingPanelSample.Window1"

    xmlns="https://schemas.microsoft.com/winfx/avalon/2005"

    xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005"

    Title="AnnoyingPanelSample"

    xmlns:local="local"

    >

  <ScrollViewer CanContentScroll="True">

    <local:AnnoyingPanel>

      <Button>button</Button>

    </local:AnnoyingPanel>

  </ScrollViewer>

</Window>

As you can see, we are setting CanContentScroll to True, and setting up our AnnoyingPanel, and putting one button in it.

You should now have something that will compile. Compile the sample and run it, and you should get an application that fires an exception from the setter for ScrollOwner. We have already learnt something - the first thing we need to implement is the ScrollOwner property, because when the ScrollViewer learns that a CanContentScroll content element is its content, it wants to assert ownership. This is an easy thing to fix - we just add a private member to store the owner in, and write the setter and getter around that:

private ScrollViewer _owner;

public ScrollViewer ScrollOwner

{

    get { return _owner; }

    set { _owner = value; }

}

If you compile and run, you will get an exception in a different place now - the setter for CanHorizontallyScroll. This is because by default, the horizontal scrollbar is disabled and the vertical scrollbar is not. The ScrollViewer wants to tell its content about that. This is also very easy to implement - we can have two boolean properties to store this information, and write the setters and getters around that:

private bool _canHScroll = false;

private bool _canVScroll = false;

public bool CanHorizontallyScroll

{

    get { return _canHScroll; }

    set { _canHScroll=value;}

}

public bool CanVerticallyScroll

{

    get { return _canVScroll; }

    set { _canVScroll=value;}

}

Now if you compile and run, you should see a blank window. Wow - is that all? Not quite. If you try to scroll anything, then you will get more exceptions. And no layout is occuring. And the ScrollViewer is not seeing the extent of the content, so it does not know what the scroll range is. But the first step has been made!

Next post: We start writing Measure and Arrange for the Panel, and see what the next IScrollInfo methods we need are.

[update] A fellow team member read my post, and made some suggestions to clarify some stuff. Updates are marked above.