开发者

AvalonControls RangeSlider Memory Leak (DependencyPropertyDescriptor...AddValueChanged)

开发者 https://www.devze.com 2023-02-13 11:57 出处:网络
I\'ve been using the AvalonControls RangeSlider and discovered quite a memory leak when used in a DataTemplate. I have researched this and have concluded that the following is the culprit:

I've been using the AvalonControls RangeSlider and discovered quite a memory leak when used in a DataTemplate. I have researched this and have concluded that the following is the culprit:

    DependencyPropertyDescriptor.FromProperty(ActualWidthProperty, typeof(RangeSlider)).
            RemoveValueChanged(this, delegate { ReCalculateWidths(); });

That said, I don't understand where with the RangeSlider class is appropriate for the RemoveValue Changed, which i assume will solve this leak. It should be noted that I'm using the control with DataTemplate / ItemsTemplate on tabs.

The following is the class (my apologies for the lengthy snippet). Any guidance is MUCH appreciated.

{
    #region Data members
    bool internalUpdate = false;
    const double RepeatButtonMoveRatio = 0.1;//used to move the selection by x ratio when click the repeat buttons
    const double DefaultSplittersThumbWidth = 10;
    Thumb centerThumb; //the center thumb to move the range around
    Thumb leftThumb;//the left thumb that is used to expand the range selected
    Thumb rightThumb;//the right thumb that is used to expand the range selected
    RepeatButton leftButton;//the left side of the control (movable left part)
    RepeatButton rightButton;//the right side of the control (movable right part)
    StackPanel visualElementsContainer;//stackpanel to store the visual elements for this control
    #endregion

    #region properties and events

    /// <summary>
    /// The min value for the range of the range slider
    /// </summary>
    public long RangeStart
    {
        get { return (long)GetValue(RangeStartProperty); }
        set { SetValue(RangeStartProperty, value); }
    }

    /// <summary>
    /// The min value for the range of the range slider
    /// </summary>
    public static readonly DependencyProperty RangeStartProperty =
        DependencyProperty.Register("RangeStart", typeof(long), typeof(RangeSlider),
        new UIPropertyMetadata((long)0,
            delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
            {
                RangeSlider slider = (RangeSlider)sender;
                if (!slider.internalUpdate)//check if the property is set internally
                {
                    slider.ReCalculateRanges();
                    slider.ReCalculateWidths();
                }
            }));


    /// <summary>
    /// The max value for the range of the range slider
    /// </summary>
    public long RangeStop
    {
        get { return (long)GetValue(RangeStopProperty); }
        set { SetValue(RangeStopProperty, value); }
    }

    /// <summary>
    /// The max value for the range of the range slider
    /// </summary>
    public static readonly DependencyProperty RangeStopProperty =
        DependencyProperty.Register("RangeStop", typeof(long), typeof(RangeSlider),
        new UIPropertyMetadata((long)1,
            delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
            {
                RangeSlider slider = (RangeSlider)sender;
                if (!slider.internalUpdate)//check if the property is set internally
                {
                    slider.ReCalculateRanges();
                    slider.ReCalculateWidths();
                }
            }));


    /// <summary>
    /// The min value of the selected range of the range slider
    /// </summary>
    public long RangeStartSelected
    {
        get { return (long)GetValue(RangeStartSelectedProperty); }
        set { SetValue(RangeStartSelectedProperty, value); }
    }

    /// <summary>
    /// The min value of the selected range of the range slider
    /// </summary>
    public static readonly DependencyProperty RangeStartSelectedProperty =
        DependencyProperty.Register("RangeStartSelected", typeof(long), typeof(RangeSlider),
        new UIPropertyMetadata((long)0,
            delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
            {
                RangeSlider slider = (RangeSlider)sender;
                if (!slider.internalUpdate)//check if the property is set internally
                {
                    slider.ReCalculateWidths();
                    slider.OnRangeSelectionChanged(new RangeSelectionChangedEventArgs(slider));
                }
            }));


    /// <summary>
    /// The max value of the selected range of the range slider
    /// </summary>
    public long RangeStopSelected
    {
        get { return (long)GetValue(RangeStopSelectedProperty); }
        set { SetValue(RangeStopSelectedProperty, value); }
    }

    /// <summary>
    /// The max value of the selected range of the range slider
    /// </summary>
    public static readonly DependencyProperty RangeStopSelectedProperty =
        DependencyProperty.Register("RangeStopSelected", typeof(long), typeof(RangeSlider),
        new UIPropertyMetadata((long)1,
            delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
            {
                RangeSlider slider = (RangeSlider)sender;
                if (!slider.internalUpdate)//check if the property is set internally
                {
                    slider.ReCalculateWidths();
                    slider.OnRangeSelectionChanged(new RangeSelectionChangedEventArgs(slider));
                }
            }));


    /// <summary>
    /// The min range value that you can have for the range slider
    /// </summary>
    /// <exception cref="ArgumentOutOfRangeException">Thrown when MinRange is set less than 0</exception>
    public long MinRange
    {
        get { return (long)GetValue(MinRangeProperty); }
        set { SetValue(MinRangeProperty, value); }
    }

    /// <summary>
    /// The min range value that you can have for the range slider
    /// </summary>
    /// <exception cref="ArgumentOutOfRangeException">Thrown when MinRange is set less than 0</exception>
    public static readonly DependencyProperty MinRangeProperty =
        DependencyProperty.Register("MinRange", typeof(long), typeof(RangeSlider),
        new UIPropertyMetadata((long)0,
            delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
            {
                if ((long)e.NewValue < 0)
                    throw new ArgumentOutOfRangeException("value", "value for MinRange cannot be less than 0");

                RangeSlider slider = (RangeSlider)sender;
                if (!slider.internalUpdate)//check if the property is set internally
                {
                    slider.internalUpdate = true;//set flag to signal that the properties are being set by the object itself
                    slider.RangeStopSelected = Math.Max(slider.RangeStopSelected, slider.RangeStartSelected + (long)e.NewValue);
                    slider.RangeStop = Math.Max(slider.RangeStop, slider.RangeStopSelected);
                    slider.internalUpdate = false;//set flag to signal that the properties are being set by the object itself

                    slider.ReCalculateRanges();
                    slider.ReCalculateWidths();
                }
            }));

    /// <summary>
    /// Event raised whenever the selected range is changed
    /// </summary>
    public static readonly RoutedEvent RangeSelectionChangedEvent =
        EventManager.RegisterRoutedEvent("RangeSelectionChanged",
        RoutingStrategy.Bubble, typeof(RangeSelectionChangedEventHandler), typeof(RangeSlider));

    /// <summary>
    /// Event raised whenever the selected range is changed
    /// </summary>
    public event RangeSelectionChangedEventHandler RangeSelectionChanged
    {
        add { AddHandler(RangeSelectionChangedEvent, value); }
        remove { RemoveHandler(RangeSelectionChangedEvent, value); }
    }


    #endregion

    #region Commands

    /// <summary>
    /// Command to move back the selection
    /// </summary>
    public static RoutedUICommand MoveBack =
        new RoutedUICommand("MoveBack", "MoveBack", typeof(RangeSlider),
            new InputGestureCollection(new InputGesture[] {
                new KeyGesture(Key.B, ModifierKeys.Control)
            }));

    /// <summary>
    /// Command to move forward the selection
    /// </summary>
    public static RoutedUICommand MoveForward =
        new RoutedUICommand("MoveForward", "MoveForward", typeof(RangeSlider),
            new InputGestureCollection(new InputGesture[] {
                new KeyGesture(Key.F, ModifierKeys.Control)
            }));

    /// <summary>
    /// Command to move all forward the selection
    /// </summary>
    public static RoutedUICommand MoveAllForward =
        new RoutedUICommand("MoveAllForward", "MoveAllForward", typeof(RangeSlider),
            new InputGestureCollection(new InputGesture[] {
                new KeyGesture(Key.F, ModifierKeys.Alt)
            }));

    /// <summary>
    /// Command to move all back the selection
    /// </summary>
    public static RoutedUICommand MoveAllBack =
        new RoutedUICommand("MoveAllBack", "MoveAllBack", typeof(RangeSlider),
            new InputGestureCollection(new InputGesture[] {
                new KeyGesture(Key.B, ModifierKeys.Alt)
            }));

    #endregion

    /// <summary>
    /// Default constructor
    /// </summary>
    public RangeSlider()
    {
        CommandBindings.Add(new CommandBinding(MoveBack, MoveBackHandler));
        CommandBindings.Add(new CommandBinding(MoveForward, MoveForwardHandler));
        CommandBindings.Add(new CommandBinding(MoveAllForward, MoveAllForwardHandler));
        CommandBindings.Add(new CommandBinding(MoveAllBack, MoveAllBackHandler));

        //hook to the size change event of the range slider
        DependencyPropertyDescriptor.FromProperty(ActualWidthProperty, typeof(RangeSlider)).
            AddValueChanged(this, delegate { ReCalculateWidths(); });

    }


    /// <summary>
    /// Static constructor
    /// </summary>
    static RangeSlider()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(RangeSlider), new FrameworkPropertyMetadata(typeof(RangeSlider))
            );
    }

    #region Command handlers

    void MoveAllBackHandler(object sender, ExecutedRoutedEventArgs e)
    {
        ResetSelection(true);
    }

    void MoveAllForwardHandler(object sender, ExecutedRoutedEventArgs e)
    {
        ResetSelection(false);
    }

    void MoveBackHandler(object sender, ExecutedRoutedEventArgs e)
    {
        MoveSelection(true);
    }

    void MoveForwardHandler(object sender, ExecutedRoutedEventArgs e)
    {
        MoveSelection(false);
    }
    #endregion

    #region event handlers for visual elements to drag the range
    //drag thumb from the right splitter
    private void RightThumbDragDelta(object sender, DragDeltaEventArgs e)
    {
        MoveThumb(centerThumb, rightButton, e.HorizontalChange);
        ReCalculateRangeSelected(false, true);
    }

    //drag thumb from the left splitter
    private void LeftThumbDragDelta(object sender, DragDeltaEventArgs e)
    {
        MoveThumb(leftButton, centerThumb, e.HorizontalChange);
        ReCalculateRangeSelected(true, false);
    }

    //left repeat button clicked
    private void LeftButtonClick(object sender, RoutedEventArgs e)
    {
        MoveSelection(true);
    }
    //right repeat button clicked
    private void RightButtonClick(object sender, RoutedEventArgs e)
    {

        MoveSelection(false);
    }

    //drag thumb from the middle
    private void CenterThumbDragDelta(object sender, DragDeltaEventArgs e)
    {
        MoveThumb(leftButton, rightButton, e.HorizontalChange);
        ReCalculateRangeSelected(true, true);
    }
    #endregion

    #region logic to resize range
    //resizes the left column and the right column
    private static void MoveThumb(FrameworkElement x, FrameworkElement y, double horizonalChange)
    {
        double change = 0;
        if (horizonalChange < 0) //slider went left
            change = GetChangeKeepPositive(x.Width, horizonalChange);
        else if (horizonalChange > 0) //slider went right if(horizontal change == 0 do nothing)
            change = -GetChangeKeepPositive(y.Width, -horizonalChange);

        x.Width += change;
        y.Width -= change;
    }

    //ensures that the new value (newValue param) is a valid value. returns false if not
    private static double GetChangeKeepPositive(double width, double increment)
    {
        return Math.Max(width + increment, 0) - width;
    }
    #endregion

    #region logic to calculate the range
    long movableRange = 0;
    double movableWidth = 0;

    //recalculates the movableRange. called from the RangeStop setter, RangeStart setter and MinRange setter
    private void ReCalculateRanges()
    {
        movableRange = RangeStop - RangeStart - MinRange;
    }

    //recalculates the movableWidth. called whenever the width of the control changes
    private void ReCalculateWidths()
    {

       if (leftButton != null && rightButton != null && centerThumb != null)
        {
            movableWidth = Math.Max(ActualWidth - rightThumb.ActualWidth - leftThumb.ActualWidth - centerThumb.MinWidth, 1);
            leftButton.Width = Math.Max(movableWidth * (RangeStartSelected - RangeStart) / movableRange, 0);
            rightButton.Width = Math.Max(movableWidth * (RangeStop - RangeStopSelected) / movableRange, 0);
            centerThumb.Width = Math.Max(ActualWidth - leftButton.Width - rightButton.Width - rightThumb.ActualWidth - leftThumb.ActualWidth, 0);
        }
    }

    //recalculates the rangeStartSelected called when the left thumb is moved and when the middle thumb is moved
    //recalculates the rangeStopSelected called when the right thumb is moved and when the middle thumb is moved
    private void ReCalculateRangeSelected(bool reCalculateStart, bool reCalculateStop)
    {
        internalUpdate = true;//set flag to signal that the properties are being set by the object itself
        if (reCalculateStart)
        {
            // Make sure to get exactly rangestart if thumb is at the start
            if (leftButton.Width == 0.0)
                RangeStartSelected = RangeStart;
            else
                RangeStartSelected =
                    Math.Max(RangeStart, (long)(RangeStart + movableRange * leftButton.Width / movableWidth));
        }

        if (reCalculateStop)
        {
            // Make sure to get exactly rangestop if thumb is at the end
            if (rightButton.Width == 0.0)
                RangeStopSelected = RangeStop;
            else
                RangeStopSelected =
                    Math.Min(RangeStop, (long)(RangeStop - movableRange * rightButton.Width / movableWidth));
        }

        internalUpdate = false;//set flag to signal that the properties are being set by the object itself

        if (reCalculateStart || reCalculateStop)
            //raise the RangeSelectionChanged event
            OnRangeSelectionChanged(new RangeSelectionChangedEventArgs(this));
    }

    /// <summary>
    /// moves the current selection with x value
    /// </summary>
    /// <param name="isLeft">True if you want to move to the left</param>
    public void MoveSelection(bool isLeft)
    {
        double widthChange = RepeatButtonMoveRatio * (RangeStopSelected - RangeStartSelected)
            * movableWidth / movableRange;

        widthChange = isLeft ? -widthChange : widthChange;
        MoveThumb(leftButton, rightButton, widthChange);
        ReCalculateRangeSelected(true, true);
    }

    /// <summary>
    /// Reset the Slider to the Start/End
    /// </summary>
    /// <param name="isStart">Pass true to reset to start point</param>
    public void ResetSelection(bool isStart)
    {
        double widthChange = RangeStop - RangeStart;
        widthChange = isStart ? -widthChange : widthChange;

        MoveThumb(leftButton, rightButton, widthChange);
        ReCalculateRangeSelected(true, true);
    }

    ///<summary>
    /// Change the range selected 
    ///</summary>
    ///<param name="span">The steps</param>
    public void MoveSelection(long span)
    {
        if (span > 0)
        {
            if (RangeStopSelected + span > RangeStop)
                span = RangeStop - RangeStopSelected;
        }
        else
        {
            if (RangeStartSelected + span < RangeStart)
                span = RangeStart - RangeStartSelected;
        }

        if (span != 0)
        {
            internalUpdate = true;//set flag to signal that the properties are being set by the object itself
            RangeStartSelected += span;
            RangeStopSelected += span;
            ReCalculateWidths();
            internalUpdate = false;//set flag to signal that the properties are being set by the object itself

            OnRangeSelectionChanged(new RangeSelectionChangedEventArgs(this));
        }
    }

    /// <summary>
    /// Sets the selected range in one go. If the selection is invalid, nothing happens.
    /// </summ开发者_开发问答ary>
    /// <param name="selectionStart">New selection start value</param>
    /// <param name="selectionStop">New selection stop value</param>
    public void SetSelectedRange(long selectionStart, long selectionStop)
    {
        long start = Math.Max(RangeStart, selectionStart);
        long stop = Math.Min(selectionStop, RangeStop);
        start = Math.Min(start, RangeStop - MinRange);
        stop = Math.Max(RangeStart + MinRange, stop);
        if (stop >= start + MinRange)
        {
            internalUpdate = true;//set flag to signal that the properties are being set by the object itself
            RangeStartSelected = start;
            RangeStopSelected = stop;
            ReCalculateWidths();
            internalUpdate = false;//set flag to signal that the properties are being set by the object itself
            OnRangeSelectionChanged(new RangeSelectionChangedEventArgs(this));
        }
    }

    /// <summary>
    /// Changes the selected range to the supplied range
    /// </summary>
    /// <param name="span">The span to zoom</param>
    public void ZoomToSpan(long span)
    {
        internalUpdate = true;//set flag to signal that the properties are being set by the object itself
        // Ensure new span is within the valid range
        span = Math.Min(span, RangeStop - RangeStart);
        span = Math.Max(span, MinRange);
        if (span == RangeStopSelected - RangeStartSelected)
            return; // No change

        // First zoom half of it to the right
        long rightChange = (span - (RangeStopSelected - RangeStartSelected)) / 2;
        long leftChange = rightChange;

        // If we will hit the right edge, spill over the leftover change to the other side
        if (rightChange > 0 && RangeStopSelected + rightChange > RangeStop)
            leftChange += rightChange - (RangeStop - RangeStopSelected);
        RangeStopSelected = Math.Min(RangeStopSelected + rightChange, RangeStop);
        rightChange = 0;

        // If we will hit the left edge and there is space on the right, add the leftover change to the other side
        if (leftChange > 0 && RangeStartSelected - leftChange < RangeStart)
            rightChange = RangeStart - (RangeStartSelected - leftChange);
        RangeStartSelected = Math.Max(RangeStartSelected - leftChange, RangeStart);
        if (rightChange > 0) // leftovers to the right
            RangeStopSelected = Math.Min(RangeStopSelected + rightChange, RangeStop);

        ReCalculateWidths();
        internalUpdate = false;//set flag to signal that the properties are being set by the object itself
        OnRangeSelectionChanged(new RangeSelectionChangedEventArgs(this));
    }
    #endregion

    //Raises the RangeSelectionChanged event
    private void OnRangeSelectionChanged(RangeSelectionChangedEventArgs e)
    {
        e.RoutedEvent = RangeSelectionChangedEvent;
        RaiseEvent(e);
    }

    /// <summary>
    /// Overide to get the visuals from the control template
    /// </summary>
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        visualElementsContainer = EnforceInstance<StackPanel>("PART_RangeSliderContainer");
        centerThumb = EnforceInstance<Thumb>("PART_MiddleThumb");
        leftButton = EnforceInstance<RepeatButton>("PART_LeftEdge");
        rightButton = EnforceInstance<RepeatButton>("PART_RightEdge");
        leftThumb = EnforceInstance<Thumb>("PART_LeftThumb");
        rightThumb = EnforceInstance<Thumb>("PART_RightThumb");
        InitializeVisualElementsContainer();
        ReCalculateWidths();
    }

    #region Helper
    T EnforceInstance<T>(string partName)
        where T : FrameworkElement, new()
    {
        T element = GetTemplateChild(partName) as T;
        if (element == null)
            element = new T();
        return element;
    }

    //adds all visual element to the conatiner
    private void InitializeVisualElementsContainer()
    {
        visualElementsContainer.Orientation = Orientation.Horizontal;
        leftThumb.Width = DefaultSplittersThumbWidth;
        leftThumb.Tag = "left";
        rightThumb.Width = DefaultSplittersThumbWidth;
        rightThumb.Tag = "right";

        //handle the drag delta
        centerThumb.DragDelta += CenterThumbDragDelta;
        leftThumb.DragDelta += LeftThumbDragDelta;
        rightThumb.DragDelta += RightThumbDragDelta;
        leftButton.Click += LeftButtonClick;
        rightButton.Click += RightButtonClick;

    }
    #endregion
}

/// <summary>
/// Delegate for the RangeSelectionChanged event
/// </summary>
/// <param name="sender">The object raising the event</param>
/// <param name="e">The event arguments</param>
public delegate void RangeSelectionChangedEventHandler(object sender, RangeSelectionChangedEventArgs e);

/// <summary>
/// Event arguments for the Range slider RangeSelectionChanged event
/// </summary>
public class RangeSelectionChangedEventArgs : RoutedEventArgs
{
    private long newRangeStart;

    /// <summary>
    /// The new range start selected in the range slider
    /// </summary>
    public long NewRangeStart
    {
        get { return newRangeStart; }
        set { newRangeStart = value; }
    }

    private long newRangeStop;

    /// <summary>
    /// The new range stop selected in the range slider
    /// </summary>
    public long NewRangeStop
    {
        get { return newRangeStop; }
        set { newRangeStop = value; }
    }

    /// <summary>
    /// sets the range start and range stop for the event args
    /// </summary>
    /// <param name="newRangeStart">The new range start set</param>
    /// <param name="newRangeStop">The new range stop set</param>
    internal RangeSelectionChangedEventArgs(long newRangeStart, long newRangeStop)
    {
        this.newRangeStart = newRangeStart;
        this.newRangeStop = newRangeStop;
    }

    /// <summary>
    /// sets the range start and range stop for the event args by using the slider RangeStartSelected and RangeStopSelected properties
    /// </summary>
    /// <param name="slider">The slider to get the info from</param>
    internal RangeSelectionChangedEventArgs(RangeSlider slider)
        : this(slider.RangeStartSelected, slider.RangeStopSelected)
    { }





}

}


I had similar issue. This article explains why and how to solve it: http://sharpfellows.com/post/Memory-Leaks-and-Dependency-Properties.aspx

0

精彩评论

暂无评论...
验证码 换一张
取 消