2

Is there an event that is raised when a child is scrolled into view and gives an indication of what child was realized?

Of course there is the ScrollChanged event, but it does not provide me with any indication of what element was scrolled into view.

Edit :

I've tried hooking up to the ScrollViewer's RequestBringIntoView Event, but it is never reached. Alternatively I also tried the same on the StackPanel containing the items as such:

XAML :

     <ScrollViewer RequestBringIntoView="ScrollViewer_RequestBringIntoView" >
        <StackPanel RequestBringIntoView="StackPanel_RequestBringIntoView">
            <Button Content="1" Height="20"/>
            <Button Content="2" Height="20"/>
            <Button Content="3" Height="20"/>
            <Button Content="4" Height="20"/>
            <Button Content="5" Height="20"/>
            <Button Content="6" Height="20"/>
            <Button Content="7" Height="20"/>
            <Button Content="8" Height="20"/>
            <Button Content="9" Height="20"/>
            <Button Content="10" Height="20"/>
            <Button Content="11" Height="20"/>
            <Button Content="12" Height="20"/>
            <Button Content="13" Height="20"/>
            <Button Content="14" Height="20"/>
            <Button Content="15" Height="20"/>
            <Button Content="16" Height="20"/>
            <Button Content="17" Height="20"/>
            <Button Content="18" Height="20"/>
            <Button Content="19" Height="20"/>
            <Button Content="20" Height="20"/>
            <Button Content="21" Height="20"/>
            <Button Content="22" Height="20"/>
            <Button Content="23" Height="20"/>
            <Button Content="24" Height="20"/>
        </StackPanel>
    </ScrollViewer>
      

They are never reached. As I understand it, the ScrollViewer calls BringIntoView on it's encapsulated child elements and they raise the RequestBringIntoView event, which I would expect to propagate up the visual tree. I guess the ScrollViewer handles that event internally. So I end up with the same problem of how to get notified when it's child is brought into view. I could hook each of them up, or maybe an ItemsControl would do that for me?

ouflak
  • 2,458
  • 10
  • 44
  • 49
eran otzap
  • 12,293
  • 20
  • 84
  • 139
  • 1
    I had to do this once. You are after the 'logical' child becoming visible whereas scrolling is mostly in the 'physical' world of pixels. My solution was to intercept Measure and Arrange methods and invoke an Action. Not altogether clean, but after that experience, I don't think there's a way to do it without leaving mud of some sort on the keyboard. – Gayot Fow Oct 16 '13 at 09:49

3 Answers3

7

I think you should look at this article which gives a way of telling if a control is visible to the viewer.

If you were to hook up a call to that custom method in your ScrollChanged handler, thus having a checked every time you scrolled, I think that would do the trick. I'll try this out myself....

Edit: It works! Here's the method:

private bool IsUserVisible(FrameworkElement element, FrameworkElement container)
{
    if (!element.IsVisible)
        return false;

    Rect bounds = element.TransformToAncestor(container).TransformBounds(new Rect(0.0, 0.0, element.ActualWidth, element.ActualHeight));
    Rect rect = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight);
    return rect.Contains(bounds.TopLeft) || rect.Contains(bounds.BottomRight);
}

And my simple code call:

private void Scroll_Changed(object sender, ScrollChangedEventArgs e)
{
    Object o = sender;
    bool elementIsVisible = false;

    foreach (FrameworkElement child in this.stackPanel1.Children)
    {
        if (child != null)
        {
            elementIsVisible = this.IsUserVisible(child, this.scroller);

            if (elementIsVisible)
            {
                 // Your logic
            }
        }
    }
}

Edit: I took a look through the source code of the ScrollViewer from the link that dev hedgehog posted and found this interesting private function:

// Returns true only if element is partly visible in the current viewport
private bool IsInViewport(ScrollContentPresenter scp, DependencyObject element)
{
     Rect viewPortRect = KeyboardNavigation.GetRectangle(scp);
     Rect elementRect = KeyboardNavigation.GetRectangle(element);
     return viewPortRect.IntersectsWith(elementRect);
}

This obviously suggests that even the ScrollViewer itself is interested in knowing what's visible and, as I expected, essentially performs the same kind of calculation as in that helper method. It might be worthwhile to download this code and see who calls this method, where, and why.

Edit: Looks like its called by OnKeyDown() and used to determine focus behavior, and that's it. Interesting....

Community
  • 1
  • 1
ouflak
  • 2,458
  • 10
  • 44
  • 49
  • the problem is , how would you get sample item .... you would have to keep track of all your items in the visual tree and iterate them each time .. – eran otzap Oct 16 '13 at 09:03
  • I would just check them all each time. It's not arduous code that's heavy on overhead. I can update with that example though you seem to already get it. Would that be a problem to go through each item that might scroll into view and check it each time this 'visibility' changes? – ouflak Oct 16 '13 at 09:33
  • 2
    i don't like this design at all , there must be a better way , thanks for the effort though +1 – eran otzap Oct 16 '13 at 09:34
  • Yeah it definitely has its issues, most notably for me, I arranged my sample scrollviewer such that two items could actually be seen. Depending on how those rectangles are calculated, you could get a 'true' even if just the top pixel of the next item is visible. In that case, you would lnot be able to tell which item is at top without a bit more extensive modification of that helper method, which is kind of cludgy. Anyway, good luck finding something that works for you. Maybe there's still something here that can get you in the right direction. Please post it if you find it! – ouflak Oct 16 '13 at 09:43
  • 2
    I understand concerns about design but in this case there won't be any impact on performance or memory so to me this seems as an acceptable solution. Good job outflak. Btw check the event IsVisibleChanged which will be triggered whenever something becomes visible. If the OP could listen to just this even he would get recieve 10 times the notification if 10 items are visible. Its a routed event and may be catched on parent's control. – dev hedgehog Oct 16 '13 at 23:57
2

New Edit: I read your question again and I realized I didn't understand you in first place.

Sorry I thought you mean you wish to be notified what children are inside Viewport of ScrollViewer when you mouse your mouse inside, or set focus betweent first visible or last visible item. That's when RequestBringIntoView comes handy.

Still there are few things which aren't clear to me:

"Is there an event that is raised when a child is scrolled into view which gives an indication of what child was realized ?" - Are you talking about normal panel or VirtualizingStackPanel?

The answer ouflak posted is not bad design at all. It is actually usual WPF.

If you still not happen with our suggestion take a look at source code of ScrollViewer.

http://www.dotnetframework.org/default.aspx/Dotnetfx_Win7_3@5@1/Dotnetfx_Win7_3@5@1/3@5@1/DEVDIV/depot/DevDiv/releases/Orcas/NetFXw7/wpf/src/Framework/System/Windows/Controls/ScrollViewer@cs/2/ScrollViewer@cs

Maybe there you will stumble upon an event you could use.

Old Edit: Is this what you looking for.

http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.requestbringintoview.aspx

dev hedgehog
  • 8,698
  • 3
  • 28
  • 55
  • This will only work if the Item is specifically RequestedIntoView. If the item comes into view by other means, such as normal scrolling, that event won't fire. – ouflak Oct 16 '13 at 08:36
  • Items are always requested into view – dev hedgehog Oct 16 '13 at 10:12
  • I actually hooked up events to this event just see if that was true from normal scrolling. The event never fired. I then specifically initiated a BringIntoView() on an item. Then the event fired. Try it yourself. – ouflak Oct 16 '13 at 10:14
  • I cannot agree with you completely on that. Whenever ScrollViewer executes a command it calls BringIntoView(..) and RequestBringIntoView is also fired. ScrollViewer uses IScrollInfo with the method MakeVisible which triggers BringIntoView(). Whenever you change VerticalScrollOffset or HorizontalScrollOffset MakeVisible will be called. You might want to add handler like this: EventManager.RegisterClassHandler to get every notification about RequestBringIntoView. – dev hedgehog Oct 16 '13 at 10:22
  • 1
    I tried what you suggest, though I'm not sure I did it right. I'm going to edit the question as it is simply not possible to post the code in these comments. – ouflak Oct 16 '13 at 10:48
  • Looks like they didn't accept my edit. Sorry. I don't know how we can get to a solution without posting the code. Should I try posting your suggestion as an answer and letting you edit the code (assuming they allow the changes) to get it right? – ouflak Oct 16 '13 at 11:17
  • I can't any documentation anywhere that states that this method is called on an item whenever it's containing ScrollViewer executes a command. Do you have a source for that? – ouflak Oct 16 '13 at 13:32
  • 1
    I am at work I will take a look at this in few hours for sure. :) – dev hedgehog Oct 16 '13 at 14:04
  • 1
    So now that I finally got home from work I took a look at this. I think I misunderstood OP about this. RequestBringIntoView can be used when you "move" yourself between the first visible item and last visible item in ScrollViewer e.g. set focus on any item and you get notified the item is in view. However the OP wishes to run though InternalChildren collection and figure out if child is visible to user or not. In such case I also agree with your solution ouflak. Btw here is source code of ScrollViewer maybe there you will find what you both looking for. I hope OP tells us exactly what he wants – dev hedgehog Oct 16 '13 at 23:49
  • I appreciate that.. Would you mind voting that answer up? I considered the possibility that even if BringIntoView did always fire, even Microsoft would have to find some way to perform a calculation to recognize an item was indeed in view, and that calculation really wouldn't look all that different from the helper method from that other question. And you would still have the issue of multiple items coming into view, and deciding which one is the one you really want. I don't think you get away from that no matter what clever built-in function/events/etc... might exist. Interesting discussion! – ouflak Oct 17 '13 at 07:09
1

Ok, another answer along a different vein, but based on dev hedgehog's suggestion. Basically the idea is that an item's BringIntoView method is always called when it is actually in view. How this is determined is a bit mysterious, and what happens if say two items are scrolled into view is unknown. However some sample code that should capture all calls of BringIntoView:

   string guid = System.Guid.NewGuid().ToString();

   RoutedEvent scrollIntoViewEvent = EventManager.RegisterRoutedEvent(
                guid, 
                RoutingStrategy.Direct, 
                typeof(RequestBringIntoViewEventHandler), 
                typeof(ScrollViewer));

   EventManager.RegisterClassHandler(typeof(ScrollViewer), scrollIntoViewEvent, new RequestBringIntoViewEventHandler(this.RequestBringIntoView_Handler), true);

And an example handler:

   private void RequestBringIntoView_Handler(object sender, RequestBringIntoViewEventArgs e)
   {
       Object o = sender;
   }

A bit of tweeking here might just get this to capture all of the BringIntoView events, which should provide a solution for the original question as this handler does have the item passed into the sender.

Community
  • 1
  • 1
ouflak
  • 2,458
  • 10
  • 44
  • 49
  • 2
    that's a good idea ,by giving it the same type of event args it would know how to raise this event as well (hopefully) :) , – eran otzap Oct 16 '13 at 11:42
  • I hope you can get this working for you. I just made a slight edit calling a different override of RegisterClasshandler that will capture events where the 'Handled' flag is set. – ouflak Oct 16 '13 at 12:02
  • i don't think it matters in this case since no one would handle this event but us. thanks , i'll try this out later and let you know . – eran otzap Oct 16 '13 at 12:09
  • This could be removed. I understand now what OP is asking. Here is the link to ScrollViewer source code. Maybe there you will find something usefull. http://www.dotnetframework.org/default.aspx/Dotnetfx_Win7_3@5@1/Dotnetfx_Win7_3@5@1/3@5@1/DEVDIV/depot/DevDiv/releases/Orcas/NetFXw7/wpf/src/Framework/System/Windows/Controls/ScrollViewer@cs/2/ScrollViewer@cs – dev hedgehog Oct 16 '13 at 23:58
  • @ouflak i have a follow up question , where do you register the new event and handler ? , it just i fail to see how you associate the RequestBringIntoViewEvent with this new event . – eran otzap Oct 20 '13 at 07:14
  • @ouflak please have a look http://stackoverflow.com/questions/19475908/scrollviewer-notify-on-requestbringintoview – eran otzap Oct 20 '13 at 09:39