C #. Сохранение снимка динамического графика, созданного с помощью DrawingVisual - PullRequest
0 голосов
/ 13 марта 2019

Я работаю с программой, которая использует DrawingVisual для создания динамического графика, и мне нужно добавить кнопку, чтобы сохранить график в файл в заданной точке, указанной пользователем. График имеет черный фон, но его нужно изменить на версию для печати с белым фоном и черными линиями, но я все еще застреваю при захвате объекта и сохранении его в файл. Задача кажется тривиальной, но я новичок в C #, поэтому, пожалуйста, потерпите меня.

Ниже приведен код определения DrawingContext, в котором, как мне кажется, нужно начинать с изменений.

namespace ATEGUI.Graphing.Presentation.CustomControls
{
    public class Graph : Panel
    {
        private const double LogScaleSmallGridDivs = 10;

        public double MinX
        {
            get { return (double)GetValue(MinXProperty); }
            set { SetValue(MinXProperty, value); }
        }

        public static readonly DependencyProperty MinXProperty =
            DependencyProperty.Register("MinX", typeof(double), typeof(Graph), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));


        public double MinY
        {
            get { return (double)GetValue(MinYProperty); }
            set { SetValue(MinYProperty, value); }
        }

        public static readonly DependencyProperty MinYProperty =
            DependencyProperty.Register("MinY", typeof(double), typeof(Graph), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));


        public double MaxX
        {
            get { return (double)GetValue(MaxXProperty); }
            set { SetValue(MaxXProperty, value); }
        }

        public static readonly DependencyProperty MaxXProperty =
            DependencyProperty.Register("MaxX", typeof(double), typeof(Graph), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));


        public double MaxY
        {
            get { return (double)GetValue(MaxYProperty); }
            set { SetValue(MaxYProperty, value); }
        }

        public static readonly DependencyProperty MaxYProperty =
            DependencyProperty.Register("MaxY", typeof(double), typeof(Graph), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));


        public bool AutoScaleX
        {
            get { return (bool)GetValue(AutoScaleXProperty); }
            set { SetValue(AutoScaleXProperty, value); }
        }

        public static readonly DependencyProperty AutoScaleXProperty =
            DependencyProperty.Register("AutoScaleX", typeof(bool), typeof(Graph), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));


        public bool AutoScaleY
        {
            get { return (bool)GetValue(AutoScaleYProperty); }
            set { SetValue(AutoScaleYProperty, value); }
        }

        public static readonly DependencyProperty AutoScaleYProperty =
            DependencyProperty.Register("AutoScaleY", typeof(bool), typeof(Graph), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));


        public double LogScaleX
        {
            get { return (double)GetValue(LogScaleXProperty); }
            set { SetValue(LogScaleXProperty, value); }
        }

        public static readonly DependencyProperty LogScaleXProperty =
            DependencyProperty.Register("LogScaleX", typeof(double), typeof(Graph), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));


        public double LogScaleY
        {
            get { return (double)GetValue(LogScaleYProperty); }
            set { SetValue(LogScaleYProperty, value); }
        }

        public static readonly DependencyProperty LogScaleYProperty =
            DependencyProperty.Register("LogScaleY", typeof(double), typeof(Graph), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));


        public double Zoom
        {
            get { return (double)GetValue(ZoomProperty); }
            set { SetValue(ZoomProperty, value); }
        }

        public static readonly DependencyProperty ZoomProperty =
            DependencyProperty.Register("Zoom", typeof(double), typeof(Graph), new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsMeasure));


        public double ParentWidth
        {
            get { return (double)GetValue(ParentWidthProperty); }
            set { SetValue(ParentWidthProperty, value); }
        }

        public static readonly DependencyProperty ParentWidthProperty =
            DependencyProperty.Register("ParentWidth", typeof(double), typeof(Graph), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure));


        public double ParentHeight
        {
            get { return (double)GetValue(ParentHeightProperty); }
            set { SetValue(ParentHeightProperty, value); }
        }

        public static readonly DependencyProperty ParentHeightProperty =
            DependencyProperty.Register("ParentHeight", typeof(double), typeof(Graph), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure));


        public Brush Foreground
        {
            get { return (Brush)GetValue(ForegroundProperty); }
            set { SetValue(ForegroundProperty, value); }
        }

        public static readonly DependencyProperty ForegroundProperty =
            DependencyProperty.Register("Foreground", typeof(Brush), typeof(Graph), new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender));


        public Brush Grid
        {
            get { return (Brush)GetValue(GridProperty); }
            set { SetValue(GridProperty, value); }
        }

        public static readonly DependencyProperty GridProperty =
            DependencyProperty.Register("Grid", typeof(Brush), typeof(Graph), new FrameworkPropertyMetadata(Brushes.Gray, FrameworkPropertyMetadataOptions.AffectsRender));


        public IEnumerable Graphs
        {
            get { return (IEnumerable)GetValue(GraphsProperty); }
            set { SetValue(GraphsProperty, value); }
        }

        public static readonly DependencyProperty GraphsProperty =
            DependencyProperty.Register("Graphs", typeof(IEnumerable), typeof(Graph), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, OnPropertyChanged));


        public IEnumerable Labels
        {
            get { return (IEnumerable)GetValue(LabelsProperty); }
            set { SetValue(LabelsProperty, value); }
        }

        public static readonly DependencyProperty LabelsProperty =
            DependencyProperty.Register("Labels", typeof(IEnumerable), typeof(Graph), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, OnPropertyChanged));


        private static void OnPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            var graph = sender as Graph;
            if (graph != null)
            {
                if (e.Property == GraphsProperty || e.Property == LabelsProperty)
                    graph.OnCollectionChanged(e.NewValue, e.OldValue);
            }
        }

        public void OnCollectionChanged(object newValue, object oldValue)
        {
            if (newValue is INotifyCollectionChanged)
                AddWeakEventListener((INotifyCollectionChanged)newValue, HandleCollectionChanged);
            if (oldValue is INotifyCollectionChanged)
                RemoveWeakEventListener((INotifyCollectionChanged)oldValue, HandleCollectionChanged);
        }

        private void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            this.InvalidateVisual();
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            return new Size(ParentWidth * Math.Max(Zoom, 1), ParentHeight * Math.Max(Zoom, 1));
        }

        /*
        protected override Size ArrangeOverride(Size finalSize)
        {
            var size = base.ArrangeOverride(finalSize);
            return new Size(size.Width * Math.Max(Zoom, 1), size.Height * Math.Max(Zoom, 1));
        }*/

        protected override void OnRender(DrawingContext dc)
        {
            if (ActualWidth > 0 && ActualHeight > 0)
            {
                var fontSizeValue = GetValue(TextElement.FontSizeProperty);
                double fontSize = fontSizeValue is double ? (double)fontSizeValue : 12;
                dc.DrawRectangle(Background, null, new Rect(0, 0, ActualWidth, ActualHeight));
                DoAutoScale();
                DrawAxis(dc, fontSize);
                DrawGraphs(dc, fontSize);
                DrawLabels(dc, fontSize);
            }
        }

        private void DoAutoScale()
        {
            if (AutoScaleX)
            {
                //Compute Auto-scale
                if (Graphs != null && Graphs.OfType<Model.Graph>().Any())
                {
                    double min = Graphs.OfType<Model.Graph>().Select(t => t.Points.Select(p => p.X).Min()).Min();
                    double max = Graphs.OfType<Model.Graph>().Select(t => t.Points.Select(p => p.X).Max()).Max();
                    for (int i = 0; i < 2; i++)
                    {
                        double step = RoundLog10((max - min) / 10.0);
                        min = Math.Floor(min / step) * step;
                        max = Math.Ceiling(max / step) * step;
                    }
                    MinX = min;
                    MaxX = max;
                }
                else
                {
                    MinX = 0;
                    MaxX = ActualWidth;
                }
            }
            if (AutoScaleY)
            {
                //Compute Auto-scale
                if (Graphs != null && Graphs.OfType<Model.Graph>().Any())
                {
                    double min = Graphs.OfType<Model.Graph>().Select(t => t.Points.Select(p => p.Y).Min()).Min();
                    double max = Graphs.OfType<Model.Graph>().Select(t => t.Points.Select(p => p.Y).Max()).Max();
                    for (int i = 0; i < 2; i++)
                    {
                        double step = RoundLog10((max - min) / 10.0);
                        min = Math.Floor(min / step) * step;
                        max = Math.Ceiling(max / step) * step;
                    }
                    MinY = min;
                    MaxY = max;
                }
                else
                {
                    MinY = 0;
                    MaxY = ActualHeight;
                }
            }

            //make sure min and max are valid for log scaling
            if (LogScaleX > 1.0)
            {
                if (MinX <= 0) MinX = 1;
                if (MaxX <= 0) MaxX = 1;
            }
            if (LogScaleY > 1.0)
            {
                if (MinY <= 0) MinY = 1;
                if (MaxY <= 0) MaxY = 1;
            }
        }

        private void DrawAxis(DrawingContext dc, double fontSize)
        {
            var minorGrid = Grid.Clone();
            minorGrid.Opacity = 0.25;
            //draw y axis
            if (LogScaleX > 1.0)
            {
                var minXLog = Math.Log(MinX, LogScaleX);
                var maxXLog = Math.Log(MaxX, LogScaleX);
                var stepXLog = Math.Max(1.0, Math.Ceiling((maxXLog - minXLog) / 10));
                for (double i = minXLog; i < maxXLog; i += stepXLog)
                {
                    var x = Math.Pow(LogScaleX, i);
                    DrawLine(dc, new Pen(Grid, 1), (Transform(x, MinY)), (Transform(x, MaxY)));
                    var x2 = Math.Pow(LogScaleX, i + stepXLog);
                    var w = x2 - x;
                    for (double j = 0; j < LogScaleSmallGridDivs; j++)
                    {
                        var x3 = x + w * j / LogScaleSmallGridDivs;
                        DrawLine(dc, new Pen(minorGrid, 1), (Transform(x3, MinY)), (Transform(x3, MaxY)));
                    }
                }
                double yAxisPos = Math.Max(Math.Min(0, MaxY), MinY);
                DrawLine(dc, new Pen(Foreground, 2), (Transform(MinX, yAxisPos)), (Transform(MaxX, yAxisPos)));

                for (double i = minXLog; i < maxXLog; i += stepXLog)
                {
                    var x = Math.Pow(LogScaleX, i);
                    var p = Transform(x, yAxisPos);
                    DrawLine(dc, new Pen(Foreground, 1), (new Point(p.X, p.Y + 4)), (new Point(p.X, p.Y - 4)));
                    DrawText(dc, x.ToString(), fontSize, Foreground, new Point(p.X + 2, p.Y));
                }
            }
            else
            {
                double stepX = RoundLog10((MaxX - MinX) / 10);
                for (double x = Math.Floor(MinX / stepX) * stepX; x < MaxX; x += stepX)
                    DrawLine(dc, new Pen(Grid, 1), (Transform(x, MinY)), (Transform(x, MaxY)));
                double yAxisPos = Math.Max(Math.Min(0, MaxY), MinY);
                DrawLine(dc, new Pen(Foreground, 2), (Transform(MinX, yAxisPos)), (Transform(MaxX, yAxisPos)));
                for (double x = Math.Floor(MinX / stepX) * stepX; x < MaxX; x += stepX)
                {
                    var p = Transform(x, yAxisPos);
                    DrawLine(dc, new Pen(Foreground, 1), (new Point(p.X, p.Y + 4)), (new Point(p.X, p.Y - 4)));
                    DrawText(dc, (Math.Round(x / stepX) * stepX).ToString(), fontSize, Foreground, new Point(p.X + 2, p.Y));
                }
            }

            //draw x axis
            if (LogScaleY > 1.0)
            {
                var minYLog = Math.Log(MinY, LogScaleY);
                var maxYLog = Math.Log(MaxY, LogScaleY);
                var stepYLog = Math.Max(1.0, Math.Ceiling((maxYLog - minYLog) / 10));
                for (double i = minYLog; i < maxYLog; i += stepYLog)
                {
                    var y = Math.Pow(LogScaleY, i);
                    DrawLine(dc, new Pen(Grid, 1), (Transform(MinX, y)), (Transform(MaxX, y)));
                    var y2 = Math.Pow(LogScaleY, i + stepYLog);
                    var h = y2 - y;
                    for (double j = 0; j < LogScaleSmallGridDivs; j++)
                    {
                        var y3 = y + h * j / LogScaleSmallGridDivs;
                        DrawLine(dc, new Pen(minorGrid, 1), (Transform(MinX, y3)), (Transform(MaxX, y3)));
                    }
                }
                double xAxisPos = Math.Max(Math.Min(0, MaxX), MinX);
                DrawLine(dc, new Pen(Foreground, 2), (Transform(xAxisPos, MinY)), (Transform(xAxisPos, MaxY)));

                for (double i = minYLog; i < maxYLog; i += stepYLog)
                {
                    var y = Math.Pow(LogScaleY, i);
                    var p = Transform(xAxisPos, y);
                    DrawLine(dc, new Pen(Foreground, 1), (new Point(p.X, p.Y + 4)), (new Point(p.X, p.Y - 4)));
                    DrawText(dc, y.ToString(), fontSize, Foreground, new Point(p.X + 2, p.Y));
                }
            }
            else
            {
                double stepY = RoundLog10((MaxY - MinY) / 10);
                for (double y = Math.Floor(MinY / stepY) * stepY; y < MaxY; y += stepY)
                    DrawLine(dc, new Pen(Grid, 1), (Transform(MinX, y)), (Transform(MaxX, y)));
                double xAxisPos = Math.Max(Math.Min(0, MaxX), MinX);
                DrawLine(dc, new Pen(Foreground, 2), (Transform(xAxisPos, MinY)), (Transform(xAxisPos, MaxY)));
                for (double y = Math.Floor(MinY / stepY) * stepY; y < MaxY; y += stepY)
                {
                    var p = Transform(xAxisPos, y);
                    DrawLine(dc, new Pen(Foreground, 1), (new Point(p.X + 4, p.Y)), (new Point(p.X - 4, p.Y)));
                    DrawText(dc, (Math.Round(y / stepY) * stepY).ToString(), fontSize, Foreground, new Point(p.X + 2, p.Y));
                }
            }
        }

        private void DrawLabels(DrawingContext dc, double fontSize)
        {
            if (Labels != null)
            {
                foreach (Model.Label label in Labels)
                {

                    var brush = string.IsNullOrEmpty(label.Color) ? Foreground : TypeDescriptor.GetConverter(typeof(Brush)).ConvertFromString(label.Color) as Brush;
                    if (brush == null)
                        brush = Foreground;
                    dc.DrawText(new FormattedText(label.Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, GetTypeface(), fontSize, brush), Transform(label.Location));
                }
            }
        }

        private void DrawGraphs(DrawingContext dc, double fontSize)
        {
            Point mousePos = InverseTransform(Mouse.GetPosition(this));

            if (Graphs != null)
            {
                foreach (Model.Graph graph in Graphs)
                {
                    var brush = string.IsNullOrEmpty(graph.Color) ? Foreground : TypeDescriptor.GetConverter(typeof(Brush)).ConvertFromString(graph.Color) as Brush;
                    if (brush == null)
                        brush = Foreground;
                    var pen = new Pen(brush, graph.Weight);
                    if (graph.Points.Count == 1)
                    {
                        double weightX = graph.Weight;
                        double weightY = graph.Weight;
                        dc.DrawRectangle(brush, null, new Rect(Transform(graph.Points.First()).X - weightX / 2, Transform(graph.Points.First()).Y - weightY / 2, weightX, weightY));
                    }
                    else
                    {
                        var figure = new PathFigure(Transform(graph.Points.First()), new[] { new PolyLineSegment(graph.Points.Select(t => Transform(t)), true) }, false);
                        dc.DrawGeometry(null, pen, new PathGeometry(new[] { figure }));
                    }

                    //draw mouse loc
                    if (IsMouseOver)
                    {
                        var closestPoint = graph.Points.FirstOrDefault();
                        double minDistance = Double.PositiveInfinity;
                        foreach (var point in graph.Points)
                        {
                            double distance = Math.Abs(mousePos.X - point.X);
                            if (distance < minDistance)
                            {
                                minDistance = distance;
                                closestPoint = point;
                            }
                        }
                        var closestPointTransform = Transform(closestPoint);
                        DrawLine(dc, new Pen(Grid, 0.5), (Transform(closestPoint.X, MinY)), (Transform(closestPoint.X, MaxY)));
                        DrawLine(dc, new Pen(Grid, 0.5), (Transform(MinX, closestPoint.Y)), (Transform(MaxX, closestPoint.Y)));
                        dc.DrawEllipse(Foreground, null, closestPointTransform, graph.Weight, graph.Weight);
                        dc.DrawEllipse(null, pen, closestPointTransform, graph.Weight * 3, graph.Weight * 3);
                        dc.DrawText(new FormattedText(string.Format("X:{0}\nY:{1}", closestPoint.X, closestPoint.Y), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, GetTypeface(), fontSize, brush), new Point(closestPointTransform.X + graph.Weight * 3, closestPointTransform.Y + graph.Weight * 3));
                    }
                }
            }
        }

        private void DrawText(DrawingContext dc, string text, double size, Brush brush, Point point)
        {
            var ft = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, GetTypeface(), size, brush);
            var origin = point;
            if (point.Y + ft.Height > ActualHeight)
                origin = new Point(origin.X, origin.Y - ft.Height);
            if (point.X + ft.Width > ActualWidth)
                origin = new Point(origin.X - ft.Width, origin.Y);
            dc.DrawText(ft, origin);
        }

        private void DrawLine(DrawingContext dc, Pen pen, Point p1, Point p2)
        {
            double halfPenWidth = pen.Thickness / 2.0;
            dc.PushGuidelineSet(new GuidelineSet(new double[] { p1.X + halfPenWidth, p2.X + halfPenWidth }, new double[] { p1.Y + halfPenWidth, p2.Y + halfPenWidth }));
            dc.DrawLine(pen, p1, p2);
            dc.Pop();
        }

        private double Distance(Point p1, Point p2)
        {
            return Math.Sqrt((p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y));
        }

        private Typeface GetTypeface()
        {
            return new Typeface(TextElement.GetFontFamily(this), TextElement.GetFontStyle(this), TextElement.GetFontWeight(this), TextElement.GetFontStretch(this));
        }

        private double RoundLog10(double x)
        {
            var nearestPow10 =  Math.Pow(10, Math.Floor(Math.Log10(x)));
            return Math.Floor(x / nearestPow10) * nearestPow10;
        }

        private Point Transform(Point p)
        {
            return Transform(p.X, p.Y);
        }

        private Point Transform(double x, double y)
        {
            double newX, newY;
            if (LogScaleX > 1.0)
            {
                if (x <= 0) newX = 0;
                else newX = ActualWidth * (Math.Log(x, LogScaleX) - Math.Log(MinX, LogScaleX)) / (Math.Log(MaxX, LogScaleX) - Math.Log(MinX, LogScaleX));
            }
            else
            {
                newX = ActualWidth * (x - MinX) / (MaxX - MinX);
            }
            if (LogScaleY > 1.0)
            {
                if (y <= 0) newY = ActualHeight;
                else newY = ActualHeight * (Math.Log(MaxY, LogScaleY) - Math.Log(y, LogScaleY)) / (Math.Log(MaxY, LogScaleY) - Math.Log(MinY, LogScaleY));
            }
            else
            {
                newY = ActualHeight * (MaxY - y) / (MaxY - MinY);
            }
            return new Point(newX, newY);
        }

        private Point InverseTransform(Point p)
        {
            return InverseTransform(p.X, p.Y);
        }

        private Point InverseTransform(double x, double y)
        {
            double newX, newY;
            if (LogScaleX > 1.0)
            {
                newX = Math.Pow(LogScaleX, (Math.Log(MaxX, LogScaleX) - Math.Log(MinX, LogScaleX)) * x / ActualWidth + Math.Log(MinX, LogScaleX));
            }
            else
            {
                newX = (MaxX - MinX) * x / ActualWidth + MinX;
            }
            if (LogScaleY > 1.0)
            {
                newY = Math.Pow(LogScaleY, Math.Log(MaxY, LogScaleY) - (Math.Log(MaxY, LogScaleY) - Math.Log(MinY, LogScaleY)) * y / ActualHeight);
            }
            else
            {
                newY = MaxY - (MaxY - MinY) * y / ActualHeight;
            }
            return new Point(newX, newY);
        }

        protected override void OnMouseMove(System.Windows.Input.MouseEventArgs e)
        {
            this.InvalidateVisual();
            base.OnMouseMove(e);
        }
        protected override void OnMouseLeave(MouseEventArgs e)
        {
            this.InvalidateVisual();
            base.OnMouseLeave(e);
        }


        #region WeakEventListener
        private class CollectionChangedEventListener : IWeakEventListener
        {
            private readonly INotifyCollectionChanged source;
            private readonly NotifyCollectionChangedEventHandler handler;


            public CollectionChangedEventListener(INotifyCollectionChanged source, NotifyCollectionChangedEventHandler handler)
            {
                if (source == null) { throw new ArgumentNullException("source"); }
                if (handler == null) { throw new ArgumentNullException("handler"); }
                this.source = source;
                this.handler = handler;
            }


            public INotifyCollectionChanged Source { get { return source; } }

            public NotifyCollectionChangedEventHandler Handler { get { return handler; } }


            public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
            {
                handler(sender, (NotifyCollectionChangedEventArgs)e);
                return true;
            }
        }

        [NonSerialized]
        private readonly List<CollectionChangedEventListener> collectionChangedListeners = new List<CollectionChangedEventListener>();
        /// <summary>
        /// Adds a weak event listener for a CollectionChanged event.
        /// </summary>
        /// <param name="source">The source of the event.</param>
        /// <param name="handler">The event handler.</param>
        /// <exception cref="ArgumentNullException">source must not be <c>null</c>.</exception>
        /// <exception cref="ArgumentNullException">handler must not be <c>null</c>.</exception>
        protected void AddWeakEventListener(INotifyCollectionChanged source, NotifyCollectionChangedEventHandler handler)
        {
            if (source == null) { throw new ArgumentNullException("source"); }
            if (handler == null) { throw new ArgumentNullException("handler"); }

            CollectionChangedEventListener listener = new CollectionChangedEventListener(source, handler);

            collectionChangedListeners.Add(listener);

            CollectionChangedEventManager.AddListener(source, listener);
        }

        /// <summary>
        /// Removes the weak event listener for a CollectionChanged event.
        /// </summary>
        /// <param name="source">The source of the event.</param>
        /// <param name="handler">The event handler.</param>
        /// <exception cref="ArgumentNullException">source must not be <c>null</c>.</exception>
        /// <exception cref="ArgumentNullException">handler must not be <c>null</c>.</exception>
        protected void RemoveWeakEventListener(INotifyCollectionChanged source, NotifyCollectionChangedEventHandler handler)
        {
            if (source == null) { throw new ArgumentNullException("source"); }
            if (handler == null) { throw new ArgumentNullException("handler"); }

            CollectionChangedEventListener listener = collectionChangedListeners.LastOrDefault(l =>
                l.Source == source && l.Handler == handler);

            if (listener != null)
            {
                collectionChangedListeners.Remove(listener);
                CollectionChangedEventManager.RemoveListener(source, listener);
            }
        }
        #endregion
    }
}
...