18

I'd like to change the properties of a ScrollViewer of a ListBox from C#.

I found this question here on Stackoverflow. I took the accepted answer's advice and exposed the ScrollViewer as a property of a subclass. However, this doesn't appear to be working in an example shown below. Some of the comments in that question also state that this technique didn't work.

XAML:

<Window x:Class="StackoverflowListBoxScrollViewer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">

</Window>

C#:

using System;
using System.Windows;
using System.Windows.Controls;

namespace StackoverflowListBoxScrollViewer
{
    public class MyListBox : ListBox
    {
        public ScrollViewer ScrollViewer
        { get { return (ScrollViewer)GetTemplateChild("ScrollViewer"); } }
    }

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var myListBox = new MyListBox();

            Content = myListBox;

            myListBox.Items.Add(new Button() { Content = "abc" });
            myListBox.Items.Add(new Button() { Content = "abc" });
            myListBox.Items.Add(new Button() { Content = "abc" });
            myListBox.Items.Add(new Button() { Content = "abc" });
            myListBox.Items.Add(new Button() { Content = "abc" });

            var button = new Button() { Content = "Check ScrollViewer" };
            button.Click += (s, e) =>
                {
                    if (myListBox.ScrollViewer == null)
                        Console.WriteLine("null");
                };
            myListBox.Items.Add(button);
        }
    }
}

When I click the "Check ScrollViewer" button, it prints "null". I.e., the ScrollViewer wasn't retrieved.

How do I get to that darn ScrollViewer? :-)

Community
  • 1
  • 1
dharmatech
  • 8,979
  • 8
  • 42
  • 88
  • Check also this http://stackoverflow.com/questions/3963341/get-reference-to-my-wpf-listboxs-scrollviewer-in-c – Klaus78 Apr 24 '12 at 07:22
  • ...and you really shouldn't call your ScrollViewer-Property "ScrollViewer". – basti Apr 24 '12 at 07:55
  • 2
    @chiffre: why not? It's actually in the .NET Naming Guidelines for Properties: **Consider giving a property the same name as its type.** (http://msdn.microsoft.com/en-us/library/ms229012.aspx) – Avner Shahar-Kashtan Apr 24 '12 at 08:24
  • @AvnerShahar-Kashtan: The link you are cited talks about enumerations. And enumerations only. You may fight about if it is a good practice or not. It's just my opinion that using the very same name for the type and the property would make me crazy... – basti Apr 24 '12 at 08:29
  • 5
    I know it mentioned only enums explicitly, but I feel that the idea holds merit for other types. I can't think of a better name for a ListBox's ScrollViewer than ScrollViewer. You can twist it around and call it ScrollContainer or ScrollProvider or ScrollPane, but you're not making it any clearer. – Avner Shahar-Kashtan Apr 24 '12 at 08:33

6 Answers6

27

you can try this little helper function

usage

var scrollViewer = GetDescendantByType(yourListBox, typeof(ScrollViewer)) as ScrollViewer;

helper function

public static Visual GetDescendantByType(Visual element, Type type)
{
  if (element == null) {
    return null;
  }
  if (element.GetType() == type) {
    return element;
  }
  Visual foundElement = null;
  if (element is FrameworkElement) {
    (element as FrameworkElement).ApplyTemplate();
  }
  for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++) {
    Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
    foundElement = GetDescendantByType(visual, type);
    if (foundElement != null) {
      break;
    }
  }
  return foundElement;
}

Hope this helps

punker76
  • 14,326
  • 5
  • 58
  • 96
  • Wow, this is a nice solution @punker76. It doesn't require that I subclass `ListBox` just to get to the `ScrollViewer`. I'm tempted to mark this the accepted answer. Debate welcome! :-) – dharmatech Apr 24 '12 at 08:55
  • Great theme agnostic version. I've provided a slightly modified one as an extension method with type safety. (I know this topic is old) – Samuel Jul 25 '13 at 06:52
  • `public static TDescendant GetDescendant(this Visual element)` looked pretty here ;D – Davi Fiamenghi Jan 17 '14 at 02:41
14

If you will use standard ListBox, so you can change yours getter to this one:

public class MyListBox : ListBox
{
    public ScrollViewer ScrollViewer
    {
        get 
        {
            Border border = (Border)VisualTreeHelper.GetChild(this, 0);

            return (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
        }
    }
}
stukselbax
  • 5,855
  • 3
  • 32
  • 54
9

I've modified the great answer of @punker76 to create an extension method for Visual and provide explicit return type:

   public static class Extensions
   {
      public static T GetDescendantByType<T>(this Visual element) where T:class
      {
         if (element == null)
         {
            return default(T);
         }
         if (element.GetType() == typeof(T))
         {
            return element as T;
         }
         T foundElement = null;
         if (element is FrameworkElement)
         {
            (element as FrameworkElement).ApplyTemplate();
         }
         for (var i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
         {
            var visual = VisualTreeHelper.GetChild(element, i) as Visual;
            foundElement = visual.GetDescendantByType<T>();
            if (foundElement != null)
            {
               break;
            }
         }
         return foundElement;
      }

   }

You can now call it by SomeVisual.GetDescendantByType and it returns either already a correct typed ScrollViewer or null (which is default(T))

Samuel
  • 6,126
  • 35
  • 70
1

As for me, exposing ScrollViewer as a property is a bad idea. Firstly, there is no guarantee that ScrollViewer exists in a template. Secondly, ScrollViewer works in sync with ItemsPanel and ItemContainerGenerator. Overriding this is the straight way to uncommon behavior.

WPF controls use another pattern. Their classes are like mediators between outer logical usage and inner visual representation. Your ListBox should expose properties which can be used by ScrollViewer in a template, but not ScrollViewer. By doing this, you break WPF standards, restrict your control to specific template, and allows user code to hack internal ListBox implementation.

Marat Khasanov
  • 3,808
  • 19
  • 22
1

The properties of the ScrollViewer are "attached" to the ListBox (cf. https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/attached-properties-overview). You can get or set it as for a dependency property by the functions:

public object GetValue (System.Windows.DependencyProperty dp);

and

public void SetValue (System.Windows.DependencyProperty dp, object value);

For example, for the listbox 'lb', you can write:

lb.GetValue(ScrollViewer.ActualHeightProperty); or lb.SetValue(ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollBarVisibility.Visible);

Hoddmimes
  • 161
  • 1
  • 5
0

Here's another reworked and generic version of @punker76's answer for C# 6:

public static class VisualExtensions
{
    public static T FindVisualDescendant<T>(this Visual element) where T : Visual
    {
        if (element == null)
            return null;

        var e = element as T;

        if (e != null)
            return e;

        (element as FrameworkElement)?.ApplyTemplate();

        var childrenCount = VisualTreeHelper.GetChildrenCount(element);

        for (var i = 0; i < childrenCount; i++)
        {
            var visual = VisualTreeHelper.GetChild(element, i) as Visual;

            var foundElement = visual.FindVisualDescendant<T>();

            if (foundElement != null)
                return foundElement;
        }

        return null;
    }
}
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742