2

The title says what I am attempting to do. Here is what I have:

I am using this CodeProject contribution to attach a RubberBand behavior to a ListBox, so that I can drag-select using the mouse. I was able to modify it, so that I can disable it during instantiation of the ListBox for when I need the ListBox to be non-interactive and only show items.

The ListBox is embeded in a UserControl and contains a canvas that displays elements and in one section of my program I needed the UserControl to be a non-interactive representation of those elements, whereas in the other I needed it to be interactive. However now, I need to be able to toggle between these two states and unfortunately that does not work with the implementation I have ATM and I do not understand why.

I have bound the attached property 'IsActive', which I added in my modified RubberBand-version (see code below) to the property 'IsEditable' of my UserControl-ViewModel, but for some reason the method 'IsActiveProperty_Changed' does not execute, when 'IsEditable' changes.

This is I am using the behavior and binding to 'IsEditable':

<i:Interaction.Behaviors>
    <behavior:RubberBandBehavior IsActive="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.IsEditable}"/>
</i:Interaction.Behaviors>

I have also tried this, which also does not work:

<behavior:RubberBandBehavior IsActive="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.IsEditable, UpdateSourceTrigger=PropertyChanged}"/>

To disable the hit-detection of the ListBox, I am also binding to 'IsEditable', which does work fine:

        <ListBox.ItemContainerStyle>
            <Style TargetType="{x:Type ListBoxItem}">

                <Style.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.IsEditable}" Value="False">
                        <Setter Property="IsHitTestVisible" Value="False" />
                        <Setter Property="Focusable" Value="False" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ListBox.ItemContainerStyle>

I therefore suspect, that it has to do with my implementation/modification of RubberBandBehavior, since I am still unexperienced to implementing Attached Properties. I hope somebody can spot my error.

Modified RubberBandBehavior.cs

public class RubberBandBehavior : Behavior<ListBox>
{
    private RubberBandAdorner band;
    private AdornerLayer adornerLayer;

    public static readonly DependencyProperty IsActiveProperty =
        DependencyProperty.Register("IsActive", typeof(bool), typeof(RubberBandBehavior),
        new PropertyMetadata(IsActiveProperty_Changed));

    private static void IsActiveProperty_Changed(DependencyObject sender,
        DependencyPropertyChangedEventArgs args)
    {
        RubberBandBehavior rubberBandBehavior = (RubberBandBehavior)sender;

        if (args.Property.Name == "IsActive")
        {
            bool newIsActiveValue = (bool)args.NewValue;
            bool oldIsActiveValue = (bool)args.OldValue;

            if (newIsActiveValue != oldIsActiveValue)
            {
                rubberBandBehavior.IsActive = newIsActiveValue;

                if (rubberBandBehavior.AssociatedObject != null)
                {
                    if (newIsActiveValue == true)
                    {
                        rubberBandBehavior.AttachBehavior();
                    }
                    else
                    {
                        rubberBandBehavior.DetachBehavior();
                    }
                }
            }
        }
    }

    public bool IsActive
    {
        get { return (bool)GetValue(IsActiveProperty); }
        set { SetValue(IsActiveProperty, value); }
    }

    protected override void OnAttached()
    {
        AssociatedObject.Loaded += new System.Windows.RoutedEventHandler(AssociatedObject_Loaded);
        base.OnAttached();
    }

    void AssociatedObject_Loaded(object sender, System.Windows.RoutedEventArgs e)
    {
        if (IsActive == true)
        {
            AttachBehavior();
        }
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Loaded -= new System.Windows.RoutedEventHandler(AssociatedObject_Loaded);
        base.OnDetaching();
    }

    private void AttachBehavior()
    {
        band = new RubberBandAdorner(AssociatedObject);
        adornerLayer = AdornerLayer.GetAdornerLayer(AssociatedObject);
        adornerLayer.Add(band);
    }

    private void DetachBehavior()
    {
        adornerLayer.Remove(band);
    }
}

RubberBandAdorner.cs:

public class RubberBandAdorner : Adorner
{
    private Point startpoint;
    private Point currentpoint;
    private Brush brush;
    private bool flag;
    private ScrollViewer viewer;
    private ScrollBar scrollbar;

    public RubberBandAdorner(UIElement adornedElement)
        :base(adornedElement)
    {
        IsHitTestVisible = false;
        adornedElement.MouseMove += new MouseEventHandler(adornedElement_PreviewMouseMove);
        adornedElement.MouseLeftButtonDown += new MouseButtonEventHandler(adornedElement_PreviewMouseLeftButtonDown);
        adornedElement.PreviewMouseLeftButtonUp += new MouseButtonEventHandler(adornedElement_PreviewMouseLeftButtonUp);
        brush = new SolidColorBrush(SystemColors.HighlightColor);
        brush.Opacity = 0.3;
    }

    void adornedElement_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        DisposeRubberBand();
    }

    void adornedElement_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        ListBox _selector = AdornedElement as ListBox;
        if (_selector.SelectedItems != null && (_selector.SelectionMode == SelectionMode.Extended || _selector.SelectionMode == SelectionMode.Multiple))
        {
            _selector.SelectedItems.Clear();
        }
        startpoint = Mouse.GetPosition(this.AdornedElement);
        Mouse.Capture(_selector);
        flag = true;
    }

    public static childItem FindVisualChild<childItem>(DependencyObject obj)
    where childItem : DependencyObject
    {
        // Search immediate children first (breadth-first)
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(obj, i);

            if (child != null && child is childItem)
                return (childItem)child;

            else
            {
                childItem childOfChild = FindVisualChild<childItem>(child);

                if (childOfChild != null)
                    return childOfChild;
            }
        }

        return null;
    }

    void adornedElement_PreviewMouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed && flag)
        {
            currentpoint = Mouse.GetPosition(AdornedElement);

            Selector _selector = AdornedElement as Selector;
            if (viewer == null)
            {
                viewer = FindVisualChild<ScrollViewer>(_selector);
            }

            if (scrollbar == null)
            {
                scrollbar = FindVisualChild<ScrollBar>(viewer);
            }

            if (_selector.Items.Count > 0)
            {
                if (currentpoint.Y > ((FrameworkElement)AdornedElement).ActualHeight && viewer.VerticalOffset < _selector.ActualHeight && scrollbar.Visibility == System.Windows.Visibility.Visible)
                {
                    startpoint.Y -= 50;
                }
                else if (currentpoint.Y < 0 && viewer.VerticalOffset > 0 && scrollbar.Visibility == System.Windows.Visibility.Visible)
                {
                    startpoint.Y += 50;
                }
            }

            InvalidateVisual();

            foreach (var obj in _selector.Items)
            {
                ListBoxItem item = _selector.ItemContainerGenerator.ContainerFromItem(obj) as ListBoxItem;
                if (item != null)
                {
                    Point point = item.TransformToAncestor(AdornedElement).Transform(new Point(0, 0));
                    Rect bandrect = new Rect(startpoint, currentpoint);
                    Rect elementrect = new Rect(point.X, point.Y, item.ActualWidth, item.ActualHeight);
                    if (bandrect.IntersectsWith(elementrect))
                    {
                        item.IsSelected = true;
                    }
                    else
                    {
                        item.IsSelected = false;
                    }
                }
            }
        }
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        Rect rect = new Rect(startpoint, currentpoint);
        drawingContext.DrawGeometry(brush, new Pen(SystemColors.HighlightBrush, 1), new RectangleGeometry(rect));
        base.OnRender(drawingContext);
    }

    private void DisposeRubberBand()
    {
        currentpoint = new Point(0, 0);
        startpoint = new Point(0, 0);
        AdornedElement.ReleaseMouseCapture();
        InvalidateVisual();
        flag = false;
    }
}

Update:

Here is the code for the IsEditable property of the ViewModel. Note that I am using the RaisePropertyChanged method from MvvmLight:

private bool isEditable;
public bool IsEditable
{
    get { return isEditable; }
    set {
        if(value != isEditable)
        {
            isEditable = value;
            RaisePropertyChanged("IsEditable");
        }
    }
}
packoman
  • 1,230
  • 1
  • 16
  • 36

1 Answers1

0

Your problem is that IsActive is NOT an AttachedProperty, just a regular DependencyProperty.

Remove the DP. This code should be removed:

public static readonly DependencyProperty IsActiveProperty =
        DependencyProperty.Register("IsActive", typeof(bool), typeof(RubberBandBehavior),
        new PropertyMetadata(IsActiveProperty_Changed));

public bool IsActive
{
    get { return (bool)GetValue(IsActiveProperty); }
    set { SetValue(IsActiveProperty, value); }
}

And then add IsActive as an attached property:

public static bool GetIsActive(DependencyObject obj)
{
    return (bool)obj.GetValue(IsActiveProperty);
}
public static void SetIsActive(DependencyObject obj, bool value)
{
    obj.SetValue(IsActiveProperty, value);
}
public static readonly DependencyProperty IsActiveProperty =
    DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(RubberBandBehavior), new PropertyMetadata(IsActiveProperty_Changed));

You will have to change the code that was setting/getting IsActive as well:

rubberBandBehavior.IsActive = newIsActiveValue;

becomes

rubberBandBehavior.SetValue(RubberBandBehavior.IsActiveProperty, newIsActiveValue);

And

if (IsActive == true)

becomes

if (this.GetValue(IsActiveProperty).Equals(true))

Although, I should mention that it is not necessary to execute the SetValue line since it would already be set to the newIsActiveValue... shouldn't hurt anything but it doesn't really do anything either. Nor is it necessary to check if the old and new values are different, if they weren't different then IsActiveProperty_Changed would not have been called.

Edit:

Here is the complete RubberBandBehavior.cs:

public class RubberBandBehavior : Behavior<ListBox>
{
    private RubberBandAdorner band;
    private AdornerLayer adornerLayer;

    public static bool GetIsActive(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsActiveProperty);
    }
    public static void SetIsActive(DependencyObject obj, bool value)
    {
        obj.SetValue(IsActiveProperty, value);
    }
    public static readonly DependencyProperty IsActiveProperty =
        DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(RubberBandBehavior), new PropertyMetadata(IsActiveProperty_Changed));

    private static void IsActiveProperty_Changed(DependencyObject sender,
        DependencyPropertyChangedEventArgs args)
    {
        RubberBandBehavior rubberBandBehavior = (RubberBandBehavior)sender;
        if (args.Property.Name == "IsActive")
        {
            bool newIsActiveValue = (bool)args.NewValue;
            if (rubberBandBehavior.AssociatedObject != null)
            {
                if (newIsActiveValue == true)
                {
                    rubberBandBehavior.AttachBehavior();
                }
                else
                {
                    rubberBandBehavior.DetachBehavior();
                }
            }
        }
    }

    protected override void OnAttached()
    {
        AssociatedObject.Loaded += new System.Windows.RoutedEventHandler(AssociatedObject_Loaded);
        base.OnAttached();
    }

    void AssociatedObject_Loaded(object sender, System.Windows.RoutedEventArgs e)
    {
        if (this.GetValue(IsActiveProperty).Equals(true))
        {
            AttachBehavior();
        }
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Loaded -= new System.Windows.RoutedEventHandler(AssociatedObject_Loaded);
        base.OnDetaching();
    }

    private void AttachBehavior()
    {
        band = new RubberBandAdorner(AssociatedObject);
        adornerLayer = AdornerLayer.GetAdornerLayer(AssociatedObject);
        adornerLayer.Add(band);
    }

    private void DetachBehavior()
    {
        adornerLayer.Remove(band);
    }
}

RubberBandAdorner is using ContainerFromItem which will not work when your items are the same (a list of strings with the same text, for example). I have modified the code to use ContainerFromIndex. The outer foreach has been changed to a for loop.

In RubberBandAdorner.cs, in the adornedElement_PreviewMouseMove method update part of the code to this:

//foreach (var obj in _selector.Items)
//{
//    ListBoxItem item = _selector.ItemContainerGenerator.ContainerFromItem(obj) as ListBoxItem;
for (int i=0; i<_selector.Items.Count; i++)
{
    ListBoxItem item =_selector.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxItem;
    if (item != null)
    {
        Point point = item.TransformToAncestor(AdornedElement).Transform(new Point(0, 0));
        Rect bandrect = new Rect(startpoint, currentpoint);
        Rect elementrect = new Rect(point.X, point.Y, item.ActualWidth, item.ActualHeight);
        if (bandrect.IntersectsWith(elementrect))
        {
            item.IsSelected = true;
        }
        else
        {
            item.IsSelected = false;
        }
    }
}
J.H.
  • 4,232
  • 1
  • 18
  • 16
  • Hi. First of all: Thanks for the answer. Unfortunately it is still not working. After making the changes like you suggested the disabling continues to work as described in my question, but the method `IsActiveProperty_Changed`, still does not fire, when `IsEditable` changes and therefore the `RubberBand` is not disabled/enabled. Any ideas what else might be the problem? – packoman Jun 06 '16 at 09:34
  • I updated my question with the definition for the property `IsEditable` for completeness and to make sure that it isn't the culprit. – packoman Jun 06 '16 at 09:47
  • I updated my answer with the code for RubberBandBehavior.cs. It does work for me, I can't say why it isn't working for you without seeing all of the code. – J.H. Jun 06 '16 at 14:27
  • Initially, I used a list of 10 strings for the ListBox's Items that all were the string "Item". This did NOT work with the rubberband though. Once I changed the items to be "Item 1", "Item 2", etc... then it worked. RubberBandAdorner's adornedElement_PreviewMouseMove's call to _selector.ItemContainerGenerator.ContainerFromItem(obj) was returning the wrong item as it would only select the first item or a previously selected item (if string 3 was clicked on then rubber band would highlight it). I've fixed that code and will post another update to my answer. – J.H. Jun 06 '16 at 14:36