using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using Brush = System.Drawing.Brush; namespace OpenEarth.EnhancedListBoxes { [TemplatePart(Name = "PART_IndicatorList", Type = typeof (ItemsControl))] public class CheckListBox : ContentControl { public static readonly DependencyProperty CheckBrushProperty = DependencyProperty.Register("CheckBrush", typeof (Brush), typeof (CheckListBox), new PropertyMetadata( new SolidColorBrush (Colors.Black))); public static readonly DependencyProperty CheckBrushStrokeThicknessProperty = DependencyProperty.Register("CheckBrushStrokeThickness", typeof (double), typeof (CheckListBox), new PropertyMetadata(2.0)); public static readonly DependencyProperty CheckHeightWidthProperty = DependencyProperty.Register("CheckHeightWidth", typeof (double), typeof (CheckListBox), new PropertyMetadata(13.0)); private ItemsControl objIndicatorList; private ObservableCollection objIndicatorOffsets; private ListBox objListBox; static CheckListBox() { //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 (CheckListBox), new FrameworkPropertyMetadata(typeof (CheckListBox))); } public CheckListBox() { Unloaded += ListBoxSelectedItemIndicatorUnloaded; Loaded += ListBoxSelectedItemIndicatorLoaded; EventManager.RegisterClassHandler(typeof (CheckBox), ButtonBase.ClickEvent, new RoutedEventHandler(CheckBoxClicked)); } [Description("Brush used to paint the check inside the checkbox. Defaults to black."), Category("Custom")] public Brush CheckBrush { get { return (Brush) GetValue(CheckBrushProperty); } set { SetValue(CheckBrushProperty, value); } } [Description("Stroke thickness for the check inside the checkbox. Defaults to 2."), Category("Custom")] public double CheckBrushStrokeThickness { get { return Convert.ToDouble(GetValue(CheckBrushStrokeThicknessProperty)); } set { SetValue(CheckBrushStrokeThicknessProperty, value); } } [Description("Size of CheckBox. CheckBox is rendered in a square so this value is the height and width of the CheckBox. Default value is 13."), Category("Custom")] public double CheckHeightWidth { get { return Convert.ToDouble(GetValue(CheckHeightWidthProperty)); } set { SetValue(CheckHeightWidthProperty, value); } } private static void CheckBoxClicked(object sender, RoutedEventArgs e) { var cb = e.OriginalSource as CheckBox; if (cb == null) { return; } var objCheckListBoxIndicatorItem = cb.DataContext as CheckListBoxIndicatorItem; if (objCheckListBoxIndicatorItem == null) { return; } objCheckListBoxIndicatorItem.RelatedListBoxItem.IsSelected = Convert.ToBoolean(cb.IsChecked); } 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 = Content as ListBox; //this removes our references to the ListBox items if (objIndicatorOffsets != null && objIndicatorOffsets.Count > 0) { objIndicatorOffsets.Clear(); } return; } throw new NotSupportedException("Invalid content. CheckListBox 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 ListBoxSelectedItemIndicatorLoaded(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. 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(); } var objGen = objListBox.ItemContainerGenerator; if (objGen.Status != GeneratorStatus.ContainersGenerated) { return; } foreach (var objSelectedItem in objListBox.Items) { var lbi = objGen.ContainerFromItem(objSelectedItem) as ListBoxItem; if (lbi == null) { continue; } var objTransform = lbi.TransformToAncestor(objListBox); var pt = objTransform.Transform(new Point(0, 0)); var dblOffset = pt.Y + (lbi.ActualHeight/2) - (CheckHeightWidth/2); objIndicatorOffsets.Add(new CheckListBoxIndicatorItem(dblOffset, lbi.IsSelected, lbi)); } } 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 ListBoxSelectedItemIndicatorUnloaded(object sender, RoutedEventArgs e) { RemoveHandler(Selector.SelectionChangedEvent, new SelectionChangedEventHandler(ListBoxSelectionChanged)); RemoveHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(ListBoxScrollViewerScrollChanged)); } } }