using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using System.Windows.Threading; namespace ExtendedGrid.Classes { /// /// Displays a ToolTip next to the ScrollBar thumb while it is being dragged. /// public static class ScrollingPreviewService { public static DataTemplate GetVerticalScrollingPreviewTemplate(DependencyObject obj) { return (DataTemplate) obj.GetValue(VerticalScrollingPreviewTemplateProperty); } public static void SetVerticalScrollingPreviewTemplate(DependencyObject obj, DataTemplate value) { obj.SetValue(VerticalScrollingPreviewTemplateProperty, value); } /// /// Allows for specifying a ContentTemplate for a ToolTip that will appear next to the /// vertical ScrollBar while dragging the thumb. /// public static readonly DependencyProperty VerticalScrollingPreviewTemplateProperty = DependencyProperty.RegisterAttached("VerticalScrollingPreviewTemplate", typeof (DataTemplate), typeof (ScrollingPreviewService), new FrameworkPropertyMetadata(null, OnVerticalScrollingPreviewTemplateChanged)); private static void OnVerticalScrollingPreviewTemplateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { if ((e.OldValue == null) && (e.NewValue != null)) { PostAttachToEvents(obj, (DataTemplate) e.NewValue, true); } else { throw new NotSupportedException("Cannot change the template once it has been set."); } } public static void OnVerticalScrollingPreviewTemplateChanged(ScrollViewer obj, DataTemplate datatemplate) { obj.Dispatcher.BeginInvoke((NoParamCallback)(() => AttachToEvents(obj, datatemplate, true)), DispatcherPriority.Loaded); } public static DataTemplate GetHorizontalScrollingPreviewTemplate(DependencyObject obj) { return (DataTemplate) obj.GetValue(HorizontalScrollingPreviewTemplateProperty); } public static void SetHorizontalScrollingPreviewTemplate(DependencyObject obj, DataTemplate value) { obj.SetValue(HorizontalScrollingPreviewTemplateProperty, value); } /// /// Allows for specifying a ContentTemplate for a ToolTip that will appear next to the /// horizontal ScrollBar while dragging the thumb. /// public static readonly DependencyProperty HorizontalScrollingPreviewTemplateProperty = DependencyProperty.RegisterAttached("HorizontalScrollingPreviewTemplate", typeof (DataTemplate), typeof (ScrollingPreviewService), new FrameworkPropertyMetadata(null, OnHorizontalScrollingPreviewTemplateChanged)); private static void OnHorizontalScrollingPreviewTemplateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { if ((e.OldValue == null) && (e.NewValue != null)) { PostAttachToEvents(obj, (DataTemplate) e.NewValue, false); } } #region Event Handling private static void PostAttachToEvents(DependencyObject obj, DataTemplate dataTemplate, bool vertical) { // Most likely, the control hasn't expanded its template, wait until // Loaded priority is reached before looking for elements. obj.Dispatcher.BeginInvoke((NoParamCallback) (() => AttachToEvents(obj, dataTemplate, vertical)), DispatcherPriority.Loaded); } private delegate void NoParamCallback(); private const double Epsilon = 0; private static void AttachToEvents(DependencyObject obj, DataTemplate dataTemplate, bool vertical) { DependencyObject source = obj; var scrollViewer = FindElementOfType(obj as FrameworkElement); if (scrollViewer != null) { string scrollBarPartName = vertical ? "PART_VerticalScrollBar" : "PART_HorizontalScrollBar"; var scrollBar = FindName(scrollBarPartName, scrollViewer); if (scrollBar != null) { var track = FindName("PART_Track", scrollBar); if (track != null) { Thumb thumb = track.Thumb; if (thumb != null) { // At this point, all of the control parts have been found. // Attach to DragStarted to open the tooltip thumb.DragStarted += delegate { var parentGrid = FindControls.FindParent(source); if(parentGrid!=null) parentGrid.IsVerticalScrolling = true; if(dataTemplate==null) return; ScrollingPreviewData data; if (_previewToolTip == null) { // Create the ToolTip if this is the first time it's been used. _previewToolTip = new ToolTip(); data = new ScrollingPreviewData(); _previewToolTip.Content = data; } else { data = _previewToolTip.Content as ScrollingPreviewData; } // Update the content in the ToolTip if (data != null) { data.UpdateScrollingValues(scrollBar); if (source is ScrollViewer) { parentGrid = FindControls.FindParent(source); if (parentGrid != null) { data.UpdateItem(parentGrid, vertical); } } else { data.UpdateItem(source as ItemsControl, vertical); } } // Set the Placement and the PlacementTarget _previewToolTip.PlacementTarget = thumb; _previewToolTip.Placement = vertical ? PlacementMode.Left : PlacementMode.Top; _previewToolTip.VerticalOffset = 0.0; _previewToolTip.HorizontalOffset = 0.0; _previewToolTip.ContentTemplate = dataTemplate; _previewToolTip.IsOpen = true; }; // Attach to DragDelta to update the ToolTip's position thumb.DragDelta += delegate { if (dataTemplate == null) return; if ((_previewToolTip != null) && // Check that we're within the range of the ScrollBar (scrollBar.Value > scrollBar.Minimum) && (scrollBar.Value < scrollBar.Maximum)) { // This is a little trick to cause the ToolTip to update its position next to the Thumb if (vertical) { _previewToolTip.VerticalOffset = Math.Abs(_previewToolTip.VerticalOffset - 0.0) < Epsilon ? 0.001 : 0.0; } else { _previewToolTip.HorizontalOffset = Math.Abs(_previewToolTip.HorizontalOffset - 0.0) < Epsilon ? 0.001 : 0.0; } } }; // Attach to DragCompleted to close the ToolTip thumb.DragCompleted += delegate { var parentGrid = FindControls.FindParent(source); if (parentGrid != null) parentGrid.IsVerticalScrolling = false; if (dataTemplate == null) return; if (_previewToolTip != null) { _previewToolTip.IsOpen = false; } }; // Attach to the Scroll event to update the ToolTip content scrollBar.Scroll += delegate { if (dataTemplate == null) return; if (_previewToolTip != null) { // The ScrollBar's value isn't updated quite yet, so // wait until Input priority scrollBar.Dispatcher.BeginInvoke((NoParamCallback) delegate { var data = ( ScrollingPreviewData ) _previewToolTip . Content; data . UpdateScrollingValues (scrollBar); data . UpdateItem (source as ItemsControl, vertical); }, DispatcherPriority.Input); } }; return; } } } // At this point, something wasn't found. If the computed visibility is not visible, // then add a handler to wait for it to become visible. if ((vertical ? scrollViewer.ComputedVerticalScrollBarVisibility : scrollViewer.ComputedHorizontalScrollBarVisibility) != Visibility.Visible) { DependencyPropertyDescriptor propertyDescriptor = DependencyPropertyDescriptor.FromProperty( vertical ? ScrollViewer.ComputedVerticalScrollBarVisibilityProperty : ScrollViewer.ComputedHorizontalScrollBarVisibilityProperty, typeof (ScrollViewer)); if (propertyDescriptor != null) { EventHandler handler = delegate { if (dataTemplate == null) return; if ((vertical ? scrollViewer.ComputedVerticalScrollBarVisibility : scrollViewer.ComputedHorizontalScrollBarVisibility) == Visibility.Visible) { EventHandler storedHandler = GetWaitForVisibleScrollBar(source); propertyDescriptor.RemoveValueChanged(scrollViewer, storedHandler); PostAttachToEvents(obj, dataTemplate, vertical); } }; SetWaitForVisibleScrollBar(source, handler); propertyDescriptor.AddValueChanged(scrollViewer, handler); } } } } private static EventHandler GetWaitForVisibleScrollBar(DependencyObject obj) { return (EventHandler) obj.GetValue(WaitForVisibleScrollBarProperty); } private static void SetWaitForVisibleScrollBar(DependencyObject obj, EventHandler value) { obj.SetValue(WaitForVisibleScrollBarProperty, value); } /// /// Storage for the property change handler if waiting for a ScrollBar to appear /// for the first time. /// private static readonly DependencyProperty WaitForVisibleScrollBarProperty = DependencyProperty.RegisterAttached("WaitForVisibleScrollBar", typeof (EventHandler), typeof (ScrollingPreviewService), new UIPropertyMetadata(null)); // Keep one instance of a ToolTip and re-use it [ThreadStatic] private static ToolTip _previewToolTip; #endregion #region Element Search Helpers /// /// Returns the template element of the given name within the Control. /// private static T FindName(string name, Control control) where T : FrameworkElement { ControlTemplate template = control.Template; if (template != null) { return template.FindName(name, control) as T; } return null; } /// /// Searches the subtree of an element (including that element) /// for an element of a particluar type. /// private static T FindElementOfType(FrameworkElement element) where T : FrameworkElement { var correctlyTyped = element as T; if (correctlyTyped != null) { return correctlyTyped; } if (element != null) { int numChildren = VisualTreeHelper.GetChildrenCount(element); for (int i = 0; i < numChildren; i++) { var child = FindElementOfType(VisualTreeHelper.GetChild(element, i) as FrameworkElement); if (child != null) { return child; } } // Popups continue in another window, jump to that tree var popup = element as Popup; if (popup != null) { return FindElementOfType(popup.Child as FrameworkElement); } } return null; } #endregion } }