Friday, August 16, 2019

Behaviors triggered by expanders

In an earlier post on behaviors I showed how to attach a behavior to a button. However, sometimes we want to attach a behavior to a control that has state, such as a check box or an expander. This is simpler because we can use the state of the control directly as the property to trigger the behavior. It's easier to show than to describe.

Start a new WPF C# project called ScrollToMe.

Start by adding a new class called Behavior. It will contain a single attached property called ScrollTo which assumes it is attached to an expander and will scroll the expander into view when the attached property becomes true. This code will execute whenever the bound property changes value. If the new value is True, the expander will scroll into view. If you want to attach this behavior to other types of controls, tweak the code accordingly.


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

namespace Behaviors
{
    class ScrollToBehavior
    {
        public static readonly DependencyProperty ScrollToProperty = DependencyProperty.RegisterAttached("ScrollTo", typeof(bool),
            typeof(ScrollToBehavior), new PropertyMetadata(false, (o, e) =>
            {
                Expander exp = o as Expander;
                if (exp == null) return;
                if ((bool)e.NewValue)
                {
                    exp.BringIntoView();
                }
            }
        ));

        public static bool GetScrollTo(DependencyObject o)
        {
            return (bool)o.GetValue(ScrollToProperty);
        }

        public static void SetScrollTo(DependencyObject o, bool value)
        {
            o.SetValue(ScrollToProperty, value);
        }
    }
}

The MainWindow will contain some expanders. The content of the expanders will be some images called Cartoon1 - 4,  you can put whatever you want in them. Note there is a set of four expanders across the top that never scroll out of view as well as a stack of them down the page in a scroll viewer. This illustrates a common page design that provides a set of expandable panels.

The reference to the Behaviors namespace allows any page to use the behaviors you have defined there. We reuse the IsPanelXExpanded properties to track the expanded state and trigger the scroll behavior. 

<Window x:Class="ScrollToMe.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ScrollToMe"
        xmlns:b="clr-namespace:Behaviors"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight">
    <StackPanel Orientation="Vertical">
        <StackPanel Orientation="Horizontal">
            <Expander Header="Panel 1 Expander" IsExpanded="{Binding IsPanel1Expanded}"/>
            <Expander Header="Panel 2 Expander" IsExpanded="{Binding IsPanel2Expanded}"/>
            <Expander Header="Panel 3 Expander" IsExpanded="{Binding IsPanel3Expanded}"/>
            <Expander Header="Panel 4 Expander" IsExpanded="{Binding IsPanel4Expanded}"/>
        </StackPanel>
        <ScrollViewer Height="200">
            <StackPanel Orientation="Vertical">
                <Expander Header="Panel 1" IsExpanded="{Binding IsPanel1Expanded}" b:ScrollToBehavior.ScrollTo="{Binding IsPanel1Expanded}">
                    <Image Source="Cartoon1.jpg" Width="200"/>
                </Expander>
                <Expander Header="Panel 2" IsExpanded="{Binding IsPanel2Expanded}" b:ScrollToBehavior.ScrollTo="{Binding IsPanel2Expanded}">
                    <Image Source="Cartoon2.jpg" Width="200"/>
                </Expander>
                <Expander Header="Panel 3" IsExpanded="{Binding IsPanel3Expanded}" b:ScrollToBehavior.ScrollTo="{Binding IsPanel3Expanded}">
                    <Image Source="Cartoon3.jpg" Width="200"/>
                </Expander>
                <Expander Header="Panel 4" IsExpanded="{Binding IsPanel4Expanded}" b:ScrollToBehavior.ScrollTo="{Binding IsPanel4Expanded}">
                    <Image Source="Cartoon4.jpg" Width="200"/>
                </Expander>
            </StackPanel>
        </ScrollViewer>
    </StackPanel>
</Window>


The code behind looks like this. It's mainly a matter of defining the properties. It's unfortunate that C# does not support indexed properties.

using System.Windows;
using System.ComponentModel;
namespace ScrollToMe
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private bool _IsPanel1Expanded = false;
        public bool IsPanel1Expanded
        {
            get { return _IsPanel1Expanded; }
            set
            {
                _IsPanel1Expanded = value;
                PropChanged("IsPanel1Expanded");
            }
        }

        private bool _IsPanel2Expanded = false;
        public bool IsPanel2Expanded
        {
            get { return _IsPanel2Expanded; }
            set
            {
                _IsPanel2Expanded = value;
                PropChanged("IsPanel2Expanded");
            }
        }

        private bool _IsPanel3Expanded = false;
        public bool IsPanel3Expanded
        {
            get { return _IsPanel3Expanded; }
            set
            {
                _IsPanel3Expanded = value;
                PropChanged("IsPanel3Expanded");
            }
        }

        private bool _IsPanel4Expanded = false;
        public bool IsPanel4Expanded
        {
            get { return _IsPanel4Expanded; }
            set
            {
                _IsPanel4Expanded = value;
                PropChanged("IsPanel4Expanded");
            }
        }

        public MainWindow()
        {
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void PropChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}

If you run this example you can see that expanding an expander while it is not in view causes the expander to be expanded, displaying its contents, and it is also scrolled into view. Overall, if you had the expansion code in place already, adding the scroll into view behavior is simple.

Here's a video demonstration. First I expand cartoon 1 which pushes expander 4 out of view. Then I expand cartoon from the top row expanders. You can see cartoon 4 expands and is scrolled into view.