Kolumne: XAML Expertise

XAML-Tipp: WPF – Scroll Synchronization mit „ScrollSyncBehavior“
Keine Kommentare

In der Kolumne „XAML Expertise“ präsentiert Gregor Biswanger Top-How-tos zum Thema XAML. Einsteiger und fortgeschrittene XAML-Experten sollen hier durch geballtes Wissen gesättigt werden. Heute gibt es folgende Expertise: WPF – Scroll Synchronization mit „ScrollSyncBehavior“

Sie kennen mit Sicherheit den Vergleichseditor in Visual Studio. Wenn man eine alte mit einer neuen Codedatei vergleichen möchte, wird jeweils links und rechts ein Panel angezeigt. Hier ist das Scrolling zueinander synchron. Wenn Sie das Gleiche mit Inhaltssteuerelementen wie einer ListBox oder einem DataGrid umsetzen möchten, werden Sie schnell überrascht sein, dass es gar nicht so einfach ist wie erwartet. Das liegt daran, dass es kein direktes Scrollevent gibt. Und die richtigen Methoden zum Steuern der Scrollbar fehlen ebenfalls.

Die gesamte Scrollbarlogik kommt vom ScrollViewer-Steuerelement, das als Kindelement bei der ListBox oder beim DataGrid eingebettet ist. Der Zugriff darauf ist nur über den VisualTreeHelper möglich. Der ScrollViewer bietet passenderweise ein ScrollChanged-Event. Abonniert man es, können die aktuellen Scrollkoordinaten an ein weiteres Steuerelement zugewiesen werden. Auch das funktioniert nur, wenn man auf dessen ScrollViewer zugreift. Hierfür gibt es dann die ScrollToVerticalOffset– und ScrollToHorizontalOffset-Methoden. Wird diese Lösung händisch in die Code-Behind-Datei implementiert, sind nur wenige Zeilen Code nötig (Listing 1).

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

    Loaded += OnLoaded;
  }

  private void OnLoaded(object sender, RoutedEventArgs e)
  {
    var scrollViewer = GetScrollViewer(listBoxA);
    scrollViewer.ScrollChanged += OnScrollChanged;
  }

  private ScrollViewer GetScrollViewer(DependencyObject dependencyObject)
  {
    var border = VisualTreeHelper.GetChild(dependencyObject, 0);
    return (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
  }

  private void OnScrollChanged(object sender, ScrollChangedEventArgs eventArgs)
  {
    var scrollViewer = GetScrollViewer(listBoxB);

    if (scrollViewer != null)
    {
      scrollViewer.ScrollToVerticalOffset(eventArgs.VerticalOffset);
      scrollViewer.ScrollToHorizontalOffset(eventArgs.HorizontalOffset);
    }
  }
}

Etwas attraktiver wäre hingegen, wenn diese UI-Logik sauber ausgelagert würde, sodass die Code-Behind-Datei frei von Code wäre und für weitere Projekte wiederverwendet werden könnte. Genau dazu eignet sich das Schreiben eines eigenen WPF Behaviors. Dabei handelt es sich um fertige UI-Funktionalitäten, die als Snippet via Drag-and-drop auf das gewünschte Steuerelement gezogen werden. Blend für VisualStudio bietet Behaviors, wie sie schon seit Expression Blend 4 für WPF und Silverlight bekannt sind. Zum Schreiben eigener Behaviors muss vorher das Behaviors SDK installiert sein. Dazu einfach einen Rechtsklick auf das WPF-Projekt und im Kontextmenü Design in Blend auswählen. In Blend findet man die Behaviors beim Assetsfenster unter dem Punkt Behaviors. Ist das SDK nicht installiert, klicken Sie auf den vorgeschlagenen Link. Anschließend können Sie zum Test ein Behavior aus der Liste auf ein UI-Steuerelement per Drag-and-drop platzieren. So können Sie sicherstellen, dass die beiden wichtigen Assemblys System.Windows.Interactivity und Microsoft.Expression.Interactions schon einmal automatisch referenziert sind. Entfernen Sie anschließend das ungewollte Behavior.

Das Schreiben eigener Behaviors ist ganz einfach: Dazu legen wir eine neue ScrollSyncBehavior-Klasse an und verwenden den bisher vorgestellten Code-Behind-Code aus Listing 1. Passend dazu werden Behavior-spezifische Eigenheiten berücksichtigt. Damit diese Klasse zu einem Behavior wird, reicht es aus, dass diese von der Basisklasse Behavior erbt. Dass dieses Behavior auch nur für das ListBox-Steuerelement gültig ist, kann man mittels Generics bei der Basisklasse festlegen.

In Listing 2 wird der gesamte Code zum ScrollSyncBehavior angezeigt. Als Konstruktor für ein Behavior wird die OnAttached-Methode überschrieben. Das AssociatedObject-Property wird von der Basisklasse ebenfalls bereitgestellt. Diese hat den gleichen Typ wie der in Generics festgelegte. In unserem Fall ist das die ListBox. Wenn gar kein Typ festgelegt wurde, ist es normalerweise vom Typ Object, und zur Laufzeit ist es dann das Steuerelement, bei dem das Behavior eingesetzt wurde. Die zweite ListBox soll über ein Dependency Property von außen festgelegt werden, das via XAML ganz einfach mittels Element Binding durchgereicht wird.

public class ScrollSyncBehavior : Behavior<ListBox>
{
  public ListBox Second
  {
    get { return (ListBox)GetValue(SecondProperty); }
    set { SetValue(SecondProperty, value); }
  }

  public static readonly DependencyProperty SecondProperty =
    DependencyProperty.Register("Second", typeof(ListBox), typeof(ScrollSyncBehavior), new PropertyMetadata());

  protected override void OnAttached()
  {
    AssociatedObject.Loaded += OnLoaded;

      base.OnAttached();
  }

  protected override void OnDetaching()
  {
    var scrollViewer = GetScrollViewer(AssociatedObject);
    scrollViewer.ScrollChanged -= OnScrollChanged;
    AssociatedObject.Loaded -= OnLoaded;

    base.OnDetaching();
  }

  private void OnLoaded(object sender, RoutedEventArgs eventArgs)
  {
    var scrollViewer = GetScrollViewer(AssociatedObject);
    scrollViewer.ScrollChanged += OnScrollChanged;
  }

  private ScrollViewer GetScrollViewer(DependencyObject dependencyObject)
  {
    var border = VisualTreeHelper.GetChild(dependencyObject, 0);
    return (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
  }

  private void OnScrollChanged(object sender, ScrollChangedEventArgs eventArgs)
  {
    var scrollViewer = GetScrollViewer(Second);

    if (scrollViewer != null)
    {
      scrollViewer.ScrollToVerticalOffset(eventArgs.VerticalOffset);
      scrollViewer.ScrollToHorizontalOffset(eventArgs.HorizontalOffset);
    }
  }
}

Das war auch schon alles, was nötig ist, um ein eigenes Behavior schreiben zu können. Nun kann das Behavior mit Blend für Visual Studio festgelegt werden. Dazu ein Rechtsklick auf das WPF-Projekt und im Kontextmenü Design in Blend auswählen.

In Blend findet man die Behaviors beim Assetfenster unter dem Punkt Behaviors. Blend erkennt anhand der Basisklasse unser Behavior im Projekt und bietet es automatisch in der Liste an. Das ScrollSyncBehavior muss dann mittels Drag-and-drop auf eine ListBox gezogen werden (Abb. 1). Für die Konfiguration der Einstellungen eines Behaviors muss im Objects-and-Timeline-Fenster das jeweilige Control aufgeklappt werden. Hier sind alle definierten Behaviors zu finden. Nachdem das gewünschte Behavior ausgewählt wurde, sind sämtliche Einstellungen rechts im Property-Fenster zu finden. Hier wird ein Element Binding via Kontextmenü festgelegt. Beim Starten der Anwendung wird das fertige Ergebnis sofort wie gewünscht angezeigt. Abbildung 2 zeigt das fertige Ergebnis. Der generierte XAML-Code ist in Listing 3 zu sehen.

Abb. 1: Das „ScrollSyncBehavior“ für die „ListBox“-Steuerelemente festlegen

Abb. 1: Das „ScrollSyncBehavior“ für die „ListBox“-Steuerelemente festlegen

Abb. 2: Scroll Synchronization bei zwei „ListBox“-Steuerelementen

Abb. 2: Scroll Synchronization bei zwei „ListBox“-Steuerelementen

<ListBox x:Name="listBoxA" Grid.Column="0" ItemTemplate="{DynamicResource ItemTemplate}" ItemsSource="{Binding Collection}">
  <i:Interaction.Behaviors>
    <local:ScrollSyncBehavior Second="{Binding ElementName=listBoxB, Mode=OneWay}"/>
  </i:Interaction.Behaviors>
</ListBox>
<ListBox x:Name="listBoxB" Grid.Column="1" ItemTemplate="{DynamicResource ItemTemplate1}" ItemsSource="{Binding Collection}">
  <i:Interaction.Behaviors>
    <local:ScrollSyncBehavior Second="{Binding ElementName=listBoxA, Mode=OneWay}"/>
  </i:Interaction.Behaviors>
</ListBox>
Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -