using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; namespace OpenEarth.EnhancedListBoxes { [TemplatePart(Name = "PART_IndicatorList", Type = typeof (ItemsControl))] public class ListBoxWithSelectedItemIndicator : ContentControl { public static readonly DependencyProperty IndicatorBrushProperty = DependencyProperty.Register("IndicatorBrush", typeof (Brush), typeof (ListBoxWithSelectedItemIndicator), new PropertyMetadata(new LinearGradientBrush(Colors.LightBlue, Colors.Blue, new Point(0.5, 0), new Point(0.5, 1)))); public static readonly DependencyProperty IndicatorHeightWidthProperty = DependencyProperty.Register("IndicatorHeightWidth", typeof (double), typeof (ListBoxWithSelectedItemIndicator), new PropertyMetadata(16.0)); private ItemsControl objIndicatorList; private ObservableCollection objIndicatorOffsets; private ListBox objListBox; static ListBoxWithSelectedItemIndicator() { //This OverrideMetadata call tells the system that this element wants to provide a style that is different than its base class. //This style is defined in themes\generic.xaml DefaultStyleKeyProperty.OverrideMetadata(typeof (ListBoxWithSelectedItemIndicator),new FrameworkPropertyMetadata(typeof (ListBoxWithSelectedItemIndicator))); } public ListBoxWithSelectedItemIndicator() { Unloaded += ListBoxWithSelectedItemIndicatorUnloaded; Loaded += ListBoxWithSelectedItemIndicatorLoaded; } [Description("Brush used to paint the indicator. Defaults to a nice blue gradient brush"), Category("Custom")] public Brush IndicatorBrush { get { return (Brush) GetValue(IndicatorBrushProperty); } set { SetValue(IndicatorBrushProperty, value); } } [Description("Size of indictor. Indicator is rendered in a square so this value is the height and width of the indicator. Default value is 16."), Category("Custom")] public double IndicatorHeightWidth { get { return Convert.ToDouble(GetValue(IndicatorHeightWidthProperty)); } set { SetValue(IndicatorHeightWidthProperty, value); } } protected override void OnContentChanged(object oldContent, object newContent) { base.OnContentChanged(oldContent, newContent); //this is our insurance policy that the developer does not add content that is not a ListBox if (newContent == null || newContent is ListBox) { //this ensures that our reference to the child ListBox is always corect or nothing. //if the child ListBox is removed, our reference is set to Nothing //if the child ListBox is swapped out, our reference is set to the newContent objListBox = newContent as ListBox; //this removes our references to the ListBox items if (objIndicatorOffsets != null && objIndicatorOffsets.Count > 0) { objIndicatorOffsets.Clear(); } return; } throw new NotSupportedException("Invalid content. ListBoxWithSelectedItemIndicator only accepts a ListBox control as its content."); } public override void OnApplyTemplate() { base.OnApplyTemplate(); //when the template is applied, this give the developer the oppurtunity to get references to name objects in the control template. //in our case, we need a reference to the ItemsControl that holds the indicator arrows. // //what your control does in the absence of an expected object in the control template is up to the control develeper. //in my case here, without the items control, we are dead in the water. // //remember that custom controls are supposed to be Lookless. Meaning the visual and code are highly decoupled. //Any designer using Blend fully expects to be able edit the control template anyway they want. //My using the "PART_" naming convention, you indicate that this object is probably necessary for the conrol to work, but this is not true in all cases. // objIndicatorList = GetTemplateChild("PART_IndicatorList") as ItemsControl; if (objIndicatorList == null) { throw new Exception("Hey! The PART_IndicatorList is missing from the template or is not an ItemsControl. Sorry but this ItemsControl is required."); } } private void ListBoxWithSelectedItemIndicatorLoaded(object sender, RoutedEventArgs e) { if (objIndicatorList == null) { //remember how much "fun" tabs were be in VB and Access? Well... // //this is here because when you place a custom control in a tab, it loads the control once before it runs OnApplyTemplate //when the TabItem its in gets clicked (focus), OnApplyTemplate runs then Loaded runs agin. // //since OnApplyTemplate has not run yet, we are out of here return; } objIndicatorOffsets = new ObservableCollection(); objIndicatorList.ItemsSource = objIndicatorOffsets; //How cool are routed events! We can listen into the child ListBoxes activities and act accordingly. AddHandler(Selector.SelectionChangedEvent, new SelectionChangedEventHandler(ListBoxSelectionChanged)); AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(ListBoxScrollViewerScrollChanged)); UpdateIndicators(); } private void UpdateIndicators() { //This is the awesome procedure that Josh Smith authored with a few modifications if (objIndicatorOffsets == null) { return; } if (objListBox == null) { return; } if (objIndicatorOffsets.Count > 0) { objIndicatorOffsets.Clear(); } if (objListBox.SelectedItems.Count == 0) { return; } var objGen = objListBox.ItemContainerGenerator; if (objGen.Status != GeneratorStatus.ContainersGenerated) { return; } foreach (var dblOffset in from lbi in (from object objSelectedItem in objListBox.SelectedItems select objGen.ContainerFromItem(objSelectedItem)).OfType() let objTransform = lbi.TransformToAncestor(objListBox) let pt = objTransform.Transform(new Point(0, 0)) select pt.Y + (lbi.ActualHeight/2) - (IndicatorHeightWidth/2)) { objIndicatorOffsets.Add(dblOffset); } } private void ListBoxScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e) { //if the user is scrolling horizontality, no reason to run any of our attached behavior code if (e.VerticalChange == 0) { return; } UpdateIndicators(); } private void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e) { UpdateIndicators(); } private void ListBoxWithSelectedItemIndicatorUnloaded(object sender, RoutedEventArgs e) { RemoveHandler(Selector.SelectionChangedEvent, new SelectionChangedEventHandler(ListBoxSelectionChanged)); RemoveHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(ListBoxScrollViewerScrollChanged)); } } }