Free cookie consent management tool by TermsFeed Policy Generator

source: branches/HeuristicLab.RegressionSolutionGradientView/HeuristicLab.Problems.DataAnalysis.Views/3.4/GradientChart.cs @ 13842

Last change on this file since 13842 was 13842, checked in by pfleck, 8 years ago

#2597

  • Reduced memory consumption greatly by reusing existing datapoints from existing series instead of creating new series on update.
  • Rearranged methods and properties in GradientChart.
  • Added properties to set fixed axis limits instead of calculation.
File size: 20.9 KB
RevLine 
[13780]1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2016 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Collections.Generic;
[13836]24using System.Drawing;
[13817]25using System.Globalization;
[13780]26using System.Linq;
[13840]27using System.Threading;
[13837]28using System.Threading.Tasks;
[13780]29using System.Windows.Forms;
30using System.Windows.Forms.DataVisualization.Charting;
31using HeuristicLab.Common;
[13836]32using HeuristicLab.MainForm.WindowsForms;
[13780]33using HeuristicLab.Visualization.ChartControlsExtensions;
34
35namespace HeuristicLab.Problems.DataAnalysis.Views {
[13831]36  public partial class GradientChart : UserControl {
37    private ModifiableDataset sharedFixedVariables; // used for syncronising variable values between charts
[13837]38    private ModifiableDataset internalDataset; // holds the x values for each point drawn
[13780]39
[13842]40    private CancellationTokenSource cancelCurrentRecalculateSource;
[13840]41
[13842]42    private readonly List<IRegressionSolution> solutions;
43    private readonly Dictionary<IRegressionSolution, Series> seriesCache;
44    private readonly Dictionary<IRegressionSolution, Series> ciSeriesCache;
45
46    #region Properties
[13831]47    public bool ShowLegend {
48      get { return chart.Legends[0].Enabled; }
49      set { chart.Legends[0].Enabled = value; }
[13780]50    }
[13831]51    public bool ShowXAxisLabel {
52      get { return chart.ChartAreas[0].AxisX.Enabled == AxisEnabled.True; }
53      set { chart.ChartAreas[0].AxisX.Enabled = value ? AxisEnabled.True : AxisEnabled.False; }
[13780]54    }
[13831]55    public bool ShowYAxisLabel {
56      get { return chart.ChartAreas[0].AxisY.Enabled == AxisEnabled.True; }
57      set { chart.ChartAreas[0].AxisY.Enabled = value ? AxisEnabled.True : AxisEnabled.False; }
58    }
59    public bool ShowCursor {
60      get { return chart.Annotations[0].Visible; }
61      set { chart.Annotations[0].Visible = value; }
62    }
[13780]63
[13831]64    private int xAxisTicks = 5;
65    public int XAxisTicks {
66      get { return xAxisTicks; }
[13842]67      set { xAxisTicks = value; }
[13780]68    }
[13842]69    private double? fixedXAxisMin;
70    public double? FixedXAxisMin {
71      get { return fixedXAxisMin; }
72      set {
73        if ((value.HasValue && fixedXAxisMin.HasValue && !value.Value.IsAlmost(fixedXAxisMin.Value)) || (value.HasValue != fixedXAxisMin.HasValue)) {
74          fixedXAxisMin = value;
75          RecalculateInternalDataset();
76        }
77      }
78    }
79    private double? fixedXAxisMax;
80    public double? FixedXAxisMax {
81      get { return fixedXAxisMax; }
82      set {
83        if ((value.HasValue && fixedXAxisMax.HasValue && !value.Value.IsAlmost(fixedXAxisMax.Value)) || (value.HasValue != fixedXAxisMax.HasValue)) {
84          fixedXAxisMax = value;
85          RecalculateInternalDataset();
86        }
87      }
88    }
89
[13831]90    private int yAxisTicks = 5;
[13842]91    public int YAxisTicks {
[13831]92      get { return yAxisTicks; }
[13842]93      set { yAxisTicks = value; }
[13831]94    }
[13842]95    private double? fixedYAxisMin;
96    public double? FixedYAxisMin {
97      get { return fixedYAxisMin; }
98      set {
99        if ((value.HasValue && fixedYAxisMin.HasValue && !value.Value.IsAlmost(fixedYAxisMin.Value)) || (value.HasValue != fixedYAxisMin.HasValue)) {
100          fixedYAxisMin = value;
101        }
102      }
103    }
104    private double? fixedYAxisMax;
105    public double? FixedYAxisMax {
106      get { return fixedYAxisMax; }
107      set {
108        if ((value.HasValue && fixedYAxisMax.HasValue && !value.Value.IsAlmost(fixedYAxisMax.Value)) || (value.HasValue != fixedYAxisMax.HasValue)) {
109          fixedYAxisMax = value;
110        }
111      }
112    }
[13780]113
[13831]114    private double trainingMin = double.MinValue;
115    public double TrainingMin {
116      get { return trainingMin; }
[13842]117      set { trainingMin = value; }
[13780]118    }
[13831]119    private double trainingMax = double.MaxValue;
120    public double TrainingMax {
121      get { return trainingMax; }
[13842]122      set { trainingMax = value; }
[13831]123    }
[13780]124
[13831]125    private int drawingSteps = 1000;
126    public int DrawingSteps {
127      get { return drawingSteps; }
[13842]128      set {
129        if (value != drawingSteps) {
130          drawingSteps = value;
131          RecalculateInternalDataset();
132          ResizeAllSeriesData();
133        }
134      }
[13780]135    }
136
[13831]137    private string freeVariable;
138    public string FreeVariable {
139      get { return freeVariable; }
[13780]140      set {
[13831]141        if (value == freeVariable) return;
142        if (solutions.Any(s => !s.ProblemData.Dataset.DoubleVariables.Contains(value))) {
143          throw new ArgumentException("Variable does not exist in the ProblemData of the Solutions.");
144        }
145        freeVariable = value;
146        RecalculateInternalDataset();
[13780]147      }
148    }
149
[13831]150    private VerticalLineAnnotation VerticalLineAnnotation {
151      get { return (VerticalLineAnnotation)chart.Annotations.SingleOrDefault(x => x is VerticalLineAnnotation); }
[13780]152    }
[13842]153    #endregion
[13780]154
155    public GradientChart() {
156      InitializeComponent();
[13836]157
[13842]158      solutions = new List<IRegressionSolution>();
159      seriesCache = new Dictionary<IRegressionSolution, Series>();
160      ciSeriesCache = new Dictionary<IRegressionSolution, Series>();
161
[13836]162      // Configure axis
163      chart.CustomizeAllChartAreas();
164      chart.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
165      chart.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
166      chart.ChartAreas[0].CursorX.Interval = 0;
167
168      chart.ChartAreas[0].CursorY.IsUserSelectionEnabled = true;
169      chart.ChartAreas[0].AxisY.ScaleView.Zoomable = true;
170      chart.ChartAreas[0].CursorY.Interval = 0;
[13840]171
[13842]172      Disposed += GradientChart_Disposed;
[13780]173    }
[13840]174    private void GradientChart_Disposed(object sender, EventArgs e) {
[13842]175      if (cancelCurrentRecalculateSource != null) {
176        if (cancelCurrentRecalculateSource.IsCancellationRequested)
177          cancelCurrentRecalculateSource.Cancel();
[13840]178      }
179    }
180
[13842]181    public void Configure(IEnumerable<IRegressionSolution> solutions, ModifiableDataset sharedFixedVariables, string freeVariable, int drawingSteps, bool initializeAxisRanges = true) {
[13831]182      if (!SolutionsCompatible(solutions))
183        throw new ArgumentException("Solutions are not compatible with the problem data.");
184      this.freeVariable = freeVariable;
185      this.drawingSteps = drawingSteps;
[13780]186
[13842]187      this.solutions.Clear();
188      this.solutions.AddRange(solutions);
189
[13831]190      // add an event such that whenever a value is changed in the shared dataset,
191      // this change is reflected in the internal dataset (where the value becomes a whole column)
192      if (this.sharedFixedVariables != null)
193        this.sharedFixedVariables.ItemChanged -= sharedFixedVariables_ItemChanged;
194      this.sharedFixedVariables = sharedFixedVariables;
195      this.sharedFixedVariables.ItemChanged += sharedFixedVariables_ItemChanged;
[13780]196
[13842]197      RecalculateTrainingLimits(initializeAxisRanges);
[13831]198      RecalculateInternalDataset();
[13842]199
200      chart.Series.Clear();
201      seriesCache.Clear();
202      ciSeriesCache.Clear();
203      foreach (var solution in this.solutions) {
204        var series = CreateSeries(solution);
205        seriesCache.Add(solution, series.Item1);
206        if (series.Item2 != null)
207          ciSeriesCache.Add(solution, series.Item2);
208      }
209
210      ResizeAllSeriesData();
211      OrderAndColorSeries();
[13780]212    }
213
[13842]214    public async Task RecalculateAsync() {
215      if (IsDisposed
216        || sharedFixedVariables == null || !solutions.Any() || string.IsNullOrEmpty(freeVariable)
217        || trainingMin.IsAlmost(trainingMax) || trainingMin > trainingMax || drawingSteps == 0)
218        return;
[13829]219
[13842]220      statusLabel.Visible = true;
221      Update(); // immediately show label
222
223      // cancel previous recalculate call
224      if (cancelCurrentRecalculateSource != null)
225        cancelCurrentRecalculateSource.Cancel();
226      cancelCurrentRecalculateSource = new CancellationTokenSource();
227
228      // Set cursor and x-axis
229      var defaultValue = sharedFixedVariables.GetDoubleValue(freeVariable, 0);
230      VerticalLineAnnotation.X = defaultValue;
231      chart.ChartAreas[0].AxisX.Title = FreeVariable + " : " + defaultValue.ToString("N3", CultureInfo.CurrentCulture);
232      SetupAxis(chart.ChartAreas[0].AxisX, trainingMin, trainingMax, XAxisTicks, fixedXAxisMin, fixedXAxisMax);
233
234      // Update series
235      var cancellationToken = cancelCurrentRecalculateSource.Token;
236      try {
237        await UpdateSeriesData(cancellationToken);
238        chart.Update();
239
240        // Set y-axis
241        double ymin = 0, ymax = 0;
242        foreach (var vs in chart.Series.SelectMany(series => series.Points.Select(s => s.YValues))) {
243          for (int i = 0; i < vs.Length; i++) {
244            var v = vs[i];
245            if (ymin > v) ymin = v;
246            if (ymax < v) ymax = v;
247          }
248        }
249        SetupAxis(chart.ChartAreas[0].AxisY, ymin, ymax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
250        chart.ChartAreas[0].RecalculateAxesScale();
251
252        UpdateOutOfTrainingRangeStripLines();
253
254        statusLabel.Visible = false;
255        Update(); // immediately show
256      }
257      catch (OperationCanceledException) { }
258      catch (AggregateException ae) {
259        if (!ae.InnerExceptions.Any(e => e is OperationCanceledException))
260          throw;
261      }
[13831]262    }
263
[13842]264    private static void SetupAxis(Axis axis, double minValue, double maxValue, int ticks, double? fixedAxisMin, double? fixedAxisMax) {
265      double axisMin, axisMax, axisInterval;
266      ChartUtil.CalculateAxisInterval(minValue, maxValue, ticks, out axisMin, out axisMax, out axisInterval);
267      axis.Minimum = fixedAxisMin ?? axisMin;
268      axis.Maximum = fixedAxisMax ?? axisMax;
269      axis.Interval = (axisMax - axisMin) / ticks;
270    }
271
272    private void RecalculateTrainingLimits(bool initializeAxisRanges) {
273      trainingMin = solutions.Select(s => s.ProblemData.Dataset.GetDoubleValues(freeVariable, s.ProblemData.TrainingIndices).Min()).Max();
274      trainingMax = solutions.Select(s => s.ProblemData.Dataset.GetDoubleValues(freeVariable, s.ProblemData.TrainingIndices).Max()).Min();
275
276      if (initializeAxisRanges) {
277        double xmin, xmax, xinterval;
278        ChartUtil.CalculateAxisInterval(trainingMin, trainingMax, XAxisTicks, out xmin, out xmax, out xinterval);
279        FixedXAxisMin = xmin;
280        FixedXAxisMax = xmax;
281      }
282    }
283
[13831]284    private void RecalculateInternalDataset() {
285      // we expand the range in order to get nice tick intervals on the x axis
[13829]286      double xmin, xmax, xinterval;
[13831]287      ChartUtil.CalculateAxisInterval(trainingMin, trainingMax, XAxisTicks, out xmin, out xmax, out xinterval);
[13842]288
289      if (FixedXAxisMin.HasValue) xmin = FixedXAxisMin.Value;
290      if (FixedXAxisMax.HasValue) xmax = FixedXAxisMax.Value;
[13831]291      double step = (xmax - xmin) / drawingSteps;
292
[13829]293      var xvalues = new List<double>();
[13831]294      for (int i = 0; i < drawingSteps; i++)
295        xvalues.Add(xmin + i * step);
296
297      var variables = sharedFixedVariables.DoubleVariables.ToList();
298      internalDataset = new ModifiableDataset(variables,
299        variables.Select(x => x == FreeVariable
300          ? xvalues
301          : Enumerable.Repeat(sharedFixedVariables.GetDoubleValue(x, 0), xvalues.Count).ToList()
302        )
303      );
[13808]304    }
305
[13842]306    private Tuple<Series, Series> CreateSeries(IRegressionSolution solution) {
307      var series = new Series {
308        ChartType = SeriesChartType.Line,
309        Name = solution.ProblemData.TargetVariable + " " + solutions.IndexOf(solution)
310      };
311      series.LegendText = series.Name;
[13837]312
[13842]313      var confidenceBoundSolution = solution as IConfidenceBoundRegressionSolution;
314      Series confidenceIntervalSeries = null;
315      if (confidenceBoundSolution != null) {
316        confidenceIntervalSeries = new Series {
317          ChartType = SeriesChartType.Range,
318          YValuesPerPoint = 2,
319          Name = "95% Conf. Interval " + series.Name,
320          IsVisibleInLegend = false
321        };
[13836]322      }
[13842]323      return Tuple.Create(series, confidenceIntervalSeries);
324    }
[13836]325
[13842]326    private void OrderAndColorSeries() {
[13836]327      chart.SuspendRepaint();
[13842]328
[13836]329      chart.Series.Clear();
330      // Add mean series for applying palette colors
[13842]331      foreach (var solution in solutions) {
332        chart.Series.Add(seriesCache[solution]);
[13780]333      }
[13842]334
[13836]335      chart.Palette = ChartColorPalette.BrightPastel;
336      chart.ApplyPaletteColors();
337      chart.Palette = ChartColorPalette.None;
338
[13842]339      // Add confidence interval series before its coresponding series for correct z index
340      foreach (var solution in solutions) {
341        Series ciSeries;
342        if (ciSeriesCache.TryGetValue(solution, out ciSeries)) {
343          var series = seriesCache[solution];
344          ciSeries.Color =  Color.FromArgb(40, series.Color);
345          int idx = chart.Series.IndexOf(seriesCache[solution]);
346          chart.Series.Insert(idx, ciSeries);
347        }
[13836]348      }
[13842]349
[13836]350      chart.ResumeRepaint(true);
[13842]351    }
[13836]352
[13842]353    private Task UpdateSeriesData(CancellationToken cancellationToken) {
354      return Task.Run(() => {
355        Parallel.ForEach(solutions, new ParallelOptions { CancellationToken = cancellationToken }, solution => {
356          var xvalues = internalDataset.GetDoubleValues(FreeVariable).ToList();
357          var yvalues = solution.Model.GetEstimatedValues(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
[13836]358
[13842]359          var series = seriesCache[solution];
360          for (int i = 0; i < xvalues.Count; i++)
361            series.Points[i].SetValueXY(xvalues[i], yvalues[i]);
[13820]362
[13842]363          var confidenceBoundSolution = solution as IConfidenceBoundRegressionSolution;
364          if (confidenceBoundSolution != null) {
365            var confidenceIntervalSeries = ciSeriesCache[solution];
[13820]366
[13842]367            cancellationToken.ThrowIfCancellationRequested();
368            var variances =
369              confidenceBoundSolution.Model.GetEstimatedVariances(internalDataset,
370                Enumerable.Range(0, internalDataset.Rows)).ToList();
371            for (int i = 0; i < xvalues.Count; i++) {
372              var lower = yvalues[i] - 1.96 * Math.Sqrt(variances[i]);
373              var upper = yvalues[i] + 1.96 * Math.Sqrt(variances[i]);
374              confidenceIntervalSeries.Points[i].SetValueXY(xvalues[i], lower, upper);
375            }
376          }
377          cancellationToken.ThrowIfCancellationRequested();
378        });
379      }, cancellationToken);
380    }
[13840]381
[13842]382    private void ResizeAllSeriesData() {
383      var xvalues = internalDataset.GetDoubleValues(FreeVariable).ToList();
384      foreach (var solution in solutions)
385        ResizeSeriesData(solution, xvalues);
[13780]386    }
[13842]387    private void ResizeSeriesData(IRegressionSolution solution, IList<double> xvalues = null) {
388      if (xvalues == null)
389        xvalues = internalDataset.GetDoubleValues(FreeVariable).ToList();
[13780]390
[13842]391      var series = seriesCache[solution];
392      series.Points.SuspendUpdates();
393      for (int i = 0; i < xvalues.Count; i++)
394        series.Points.Add(new DataPoint(xvalues[i], 0.0));
395      series.Points.ResumeUpdates();
[13780]396
[13842]397      Series confidenceIntervalSeries;
398      if (ciSeriesCache.TryGetValue(solution, out confidenceIntervalSeries)) {
399        confidenceIntervalSeries.Points.SuspendUpdates();
400        for (int i = 0; i < xvalues.Count; i++)
401          confidenceIntervalSeries.Points.Add(new DataPoint(xvalues[i], new[] { -1.0, 1.0 }));
402        confidenceIntervalSeries.Points.ResumeUpdates();
403      }
[13780]404    }
405
[13842]406    public async Task AddSolutionAsync(IRegressionSolution solution) {
[13831]407      if (!SolutionsCompatible(solutions.Concat(new[] { solution })))
408        throw new ArgumentException("The solution is not compatible with the problem data.");
[13842]409      if (solutions.Contains(solution))
410        return;
411
[13831]412      solutions.Add(solution);
[13842]413      RecalculateTrainingLimits(true);
414
415      var series = CreateSeries(solution);
416      seriesCache.Add(solution, series.Item1);
417      if (series.Item2 != null)
418        ciSeriesCache.Add(solution, series.Item2);
419
420      ResizeSeriesData(solution);
421      OrderAndColorSeries();
422
423      await RecalculateAsync();
[13780]424    }
[13842]425    public async Task RemoveSolutionAsync(IRegressionSolution solution) {
426      if (!solutions.Remove(solution))
427        return;
428
429      RecalculateTrainingLimits(true);
430
431      seriesCache.Remove(solution);
432      ciSeriesCache.Remove(solution);
433
434      await RecalculateAsync();
[13831]435    }
[13780]436
[13831]437    private static bool SolutionsCompatible(IEnumerable<IRegressionSolution> solutions) {
438      foreach (var solution1 in solutions) {
439        var variables1 = solution1.ProblemData.Dataset.DoubleVariables;
440        foreach (var solution2 in solutions) {
441          if (solution1 == solution2)
442            continue;
443          var variables2 = solution2.ProblemData.Dataset.DoubleVariables;
444          if (!variables1.All(variables2.Contains))
445            return false;
446        }
447      }
448      return true;
[13780]449    }
450
[13842]451    private void UpdateOutOfTrainingRangeStripLines() {
[13831]452      var axisX = chart.ChartAreas[0].AxisX;
453      var lowerStripLine = axisX.StripLines[0];
454      var upperStripLine = axisX.StripLines[1];
455
456      lowerStripLine.IntervalOffset = axisX.Minimum;
457      lowerStripLine.StripWidth = trainingMin - axisX.Minimum;
458
459      upperStripLine.IntervalOffset = trainingMax;
460      upperStripLine.StripWidth = axisX.Maximum - trainingMax;
[13780]461    }
462
[13842]463    #region Events
[13817]464    public event EventHandler VariableValueChanged;
465    public void OnVariableValueChanged(object sender, EventArgs args) {
466      var changed = VariableValueChanged;
467      if (changed == null) return;
468      changed(sender, args);
469    }
470
[13842]471    private void sharedFixedVariables_ItemChanged(object o, EventArgs<int, int> e) {
472      if (o != sharedFixedVariables) return;
473      var variables = sharedFixedVariables.DoubleVariables.ToList();
474      var rowIndex = e.Value;
475      var columnIndex = e.Value2;
[13831]476
[13842]477      var variableName = variables[columnIndex];
478      if (variableName == FreeVariable) return;
479      var v = sharedFixedVariables.GetDoubleValue(variableName, rowIndex);
480      var values = new List<double>(Enumerable.Repeat(v, DrawingSteps));
481      internalDataset.ReplaceVariable(variableName, values);
[13780]482    }
483
[13842]484    private double oldCurserPosition = double.NaN;
[13818]485    private void chart_AnnotationPositionChanging(object sender, AnnotationPositionChangingEventArgs e) {
[13842]486      if (oldCurserPosition.IsAlmost(e.NewLocationX))
487        return;
488      oldCurserPosition = e.NewLocationX;
489
[13840]490      var step = (trainingMax - trainingMin) / drawingSteps;
491      e.NewLocationX = step * (long)Math.Round(e.NewLocationX / step);
492      var axisX = chart.ChartAreas[0].AxisX;
493      if (e.NewLocationX > axisX.Maximum)
494        e.NewLocationX = axisX.Maximum;
495      if (e.NewLocationX < axisX.Minimum)
496        e.NewLocationX = axisX.Minimum;
[13831]497
498      var annotation = VerticalLineAnnotation;
499      var x = annotation.X;
500      sharedFixedVariables.SetVariableValue(x, FreeVariable, 0);
501
502      chart.ChartAreas[0].AxisX.Title = FreeVariable + " : " + x.ToString("N3", CultureInfo.CurrentCulture);
503      chart.Update();
504
505      OnVariableValueChanged(this, EventArgs.Empty);
[13818]506    }
507
[13780]508    private void chart_MouseMove(object sender, MouseEventArgs e) {
[13842]509      bool hitCursor = chart.HitTest(e.X, e.Y).ChartElementType == ChartElementType.Annotation;
510      chart.Cursor = hitCursor ? Cursors.VSplit : Cursors.Default;
[13780]511    }
512
513    private void chart_FormatNumber(object sender, FormatNumberEventArgs e) {
514      if (e.ElementType == ChartElementType.AxisLabels) {
515        switch (e.Format) {
516          case "CustomAxisXFormat":
517            break;
518          case "CustomAxisYFormat":
519            var v = e.Value;
520            e.LocalizedValue = string.Format("{0,5}", v);
521            break;
522          default:
523            break;
524        }
525      }
526    }
527
[13842]528    private void chart_DragDrop(object sender, DragEventArgs e) {
[13780]529      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
530      if (data != null) {
531        var solution = data as IRegressionSolution;
[13842]532        if (!solutions.Contains(solution))
533          AddSolutionAsync(solution);
[13780]534      }
535    }
[13842]536    private void chart_DragEnter(object sender, DragEventArgs e) {
[13780]537      if (!e.Data.GetDataPresent(HeuristicLab.Common.Constants.DragDropDataFormat)) return;
538      e.Effect = DragDropEffects.None;
539
540      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
541      var regressionSolution = data as IRegressionSolution;
542      if (regressionSolution != null) {
543        e.Effect = DragDropEffects.Copy;
544      }
545    }
[13817]546    #endregion
[13780]547  }
548}
Note: See TracBrowser for help on using the repository browser.