Free cookie consent management tool by TermsFeed Policy Generator

source: branches/HeuristicLab.DatastreamAnalysis/HeuristicLab.DatastreamAnalysis/3.4/DatastreamAnalysisOptimizer.cs @ 14547

Last change on this file since 14547 was 14547, checked in by jzenisek, 7 years ago

#2719 added type and view for representing dictionary<string,double> as bar chart (implementation in progress)

File size: 21.7 KB
RevLine 
[14488]1#region License Information
[14536]2
[14488]3/* HeuristicLab
4 * Copyright (C) 2002-2016 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
5 *
6 * This file is part of HeuristicLab.
7 *
8 * HeuristicLab is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * HeuristicLab is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
20 */
[14536]21
[14488]22#endregion
23
24using System;
25using System.Collections.Generic;
[14538]26using System.ComponentModel;
[14536]27using System.Diagnostics;
[14488]28using System.Drawing;
29using System.Linq;
[14536]30using System.Threading;
31using System.Threading.Tasks;
32using HeuristicLab.Analysis;
33using HeuristicLab.Collections;
[14488]34using HeuristicLab.Common;
35using HeuristicLab.Core;
[14536]36using HeuristicLab.Data;
[14488]37using HeuristicLab.Optimization;
38using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
39using HeuristicLab.Problems.DataAnalysis;
40
41namespace HeuristicLab.DatastreamAnalysis {
[14536]42  internal enum DatastreamAnalysisOptimizerAction {
43    None,
44    Prepare,
45    Start,
46    Stop,
47    Pause
48  }
[14488]49
50  [StorableClass]
[14536]51  [Item("DatastreamAnalysis Optimizer",
52     "The main loop for evaluating ensemble models against a incoming datastream of time series fashion.")]
53  [Creatable(CreatableAttribute.Categories.Algorithms)]
54  public class DatastreamAnalysisOptimizer : Executable, IOptimizer, IStorableContent {
55    #region properties
56    public string Filename { get; set; }
[14488]57
[14536]58    private DatastreamAnalysisOptimizerAction daoAction;
[14488]59
[14536]60    public IEnumerable<IOptimizer> NestedOptimizers { get; }
[14488]61
[14536]62
[14488]63    [Storable]
[14536]64    protected ILog log;
65    public ILog Log {
66      get { return log; }
67    }
[14488]68
[14536]69    [Storable]
70    private ResultCollection results;
71
72    public ResultCollection Results {
73      get { return results; }
74    }
75
76    private CancellationTokenSource cancellationTokenSource;
77    private bool stopPending;
78    private DateTime lastUpdateTime;
[14538]79    private bool prepared;
80    private bool finished;
[14536]81
82    [Storable]
83    protected int runsCounter;
84
85    [Storable]
86    private RunCollection runs;
87
88    public RunCollection Runs
89    {
90      get { return runs; }
91      protected set
92      {
93        if (value == null) throw new ArgumentNullException();
94        if (runs != value) {
95          if (runs != null) DeregisterRunsEvents();
96          runs = value;
97          if (runs != null) RegisterRunsEvents();
98        }
99      }
100    }
101
102
103    [Storable]
104    private IItemList<RegressionEnsembleModel> ensembles;
105
106    public IItemList<RegressionEnsembleModel> Ensembles {
107      get { return ensembles; }
[14488]108      set {
[14536]109        if (value == null || value == ensembles)
[14488]110          return;
[14538]111        if(!(value is IRegressionEnsembleModel)) throw new ArgumentException("Invaid ensemble model type");
112        DeregisterEnsembleEvents();
[14536]113        ensembles = value;
[14538]114        RegisterEnsembleEvents();
115        OnEnsemblesChanged();
116        Prepare();
[14488]117      }
118    }
119
120
[14536]121    // VAR 1: datastream ~= problem data, VAR 2 (TODO): datastream = external source e.g. webservice, AMQP-Queue, etc.
122    [Storable]
123    private Datastream datastream;
124
125    public Datastream Datastream {
126      get { return datastream; }
127      set {
128        if (value == null || value == datastream)
129          return;
[14538]130        if(!(value is IDatastream)) throw new ArgumentException("Invalid datastream type");
131        DeregisterDatastreamEvents();
[14536]132        datastream = value;
[14538]133        RegisterDatastreamEvents();
134        OnDatastreamChanged();
135        Prepare();
[14536]136      }
137    }
138
139    #endregion properties
140
[14538]141    #region results properties
142    private int ResultsSlidingWindowMovements {
143      get { return ((IntValue)Results["Sliding Window Movements"].Value).Value; }
144      set { ((IntValue)Results["Sliding Window Movements"].Value).Value = value; }
145    }
146    private DataTable ResultsQualities {
[14536]147      get { return ((DataTable)Results["Qualities"].Value); }
148    }
[14538]149
[14543]150    private const string ResultsQualitiesMSE = "Mean squared error";
151    private const string ResultsQualitiesPR2 = "Pearson R²";
[14538]152
153    private DataTable ResultsTargets {
154      get { return ((DataTable) Results["Targets"].Value); }
155    }
156
157    private const string ResultsTargetsReal = "Real";
158
[14547]159    private DataBarSet ResultsQualitiesBars {
160      get { return (DataBarSet) Results["Ensemble"].Value; }
[14543]161    }
162
[14538]163    protected void SetupResults() {
164      Results.Clear();
165
166      Results.Add(new Result("Sliding Window Movements", new IntValue(0)));
167      Results.Add(new Result("Qualities", new DataTable("Qualities")));
168      Results.Add(new Result("Targets", new DataTable("Targets")));
[14547]169      Results.Add(new Result("Ensemble Comparison", new DataBarSet("Ensemble Comparison")));
[14538]170
171      ResultsTargets.Rows.Add(new DataRow(ResultsTargetsReal));
172      foreach (var ensemble in Ensembles) {
[14543]173        // targets table
[14538]174        ResultsTargets.Rows.Add(new DataRow(ensemble.Name));
[14543]175
176        // qualities (series)
177        //ResultsQualities.Rows.Add(new DataRow(ensemble.Name + " - " + ResultsQualitiesMSE));
178        ResultsQualities.Rows.Add(new DataRow(ensemble.Name + " - " + ResultsQualitiesPR2));
179
180        // qualities (bars)
[14547]181        ResultsQualitiesBars.Bars.Add(new StringValue(ensemble.Name), new DoubleValue(0.0));
[14538]182      }
183    }
184
[14536]185    #endregion
186
187    #region constructors, cloner,...
[14538]188    public DatastreamAnalysisOptimizer() : base() {
[14536]189      name = "Datastream Analysis";
190      log = new Log();
191      results = new ResultCollection();
192      ensembles = new ItemList<RegressionEnsembleModel>();
193      datastream = new Datastream();
194      runsCounter = 0;
195      runs = new RunCollection();
196      Initialize();
[14488]197    }
198
199    [StorableConstructor]
[14536]200    protected DatastreamAnalysisOptimizer(bool deserializing) : base(deserializing) {
201    }
[14488]202
203    [StorableHook(HookType.AfterDeserialization)]
204    private void AfterDeserialization() {
[14538]205      Initialize();
[14488]206    }
207
208    protected DatastreamAnalysisOptimizer(DatastreamAnalysisOptimizer original, Cloner cloner) : base(original, cloner) {
[14536]209      name = original.name;
210      log = cloner.Clone(original.log);
211      results = cloner.Clone(original.results);
212      ensembles = (ItemList<RegressionEnsembleModel>) original.Ensembles.Clone(cloner);
213      datastream = (Datastream) original.Datastream.Clone(cloner);
214      runsCounter = original.runsCounter;
215      runs = cloner.Clone(original.runs);
216      Initialize();
[14488]217    }
218
219    public override IDeepCloneable Clone(Cloner cloner) {
[14536]220      return new DatastreamAnalysisOptimizer(this, cloner);
[14488]221    }
222
[14536]223    private void Initialize() {
224      if (runs != null) RegisterRunsEvents();
[14538]225      if (datastream != null) RegisterDatastreamEvents();
226      if (ensembles != null) RegisterEnsembleEvents();
[14536]227    }
228    #endregion
[14488]229
[14536]230    #region control actions
[14488]231
[14536]232    public override void Prepare() {
[14538]233      if (ensembles == null || ensembles.Count == 0 || datastream == null || !datastream.SlidingWindowEvaluationPossible) return;
234      //if (ensembles.SelectMany(x => x.Models).Count() == 0) return;
235      base.Prepare();
236      OnPrepared();
[14488]237    }
238
[14536]239    public void Prepare(bool clearRuns) {
[14538]240      if (ensembles == null || ensembles.Count == 0 || datastream == null || !datastream.SlidingWindowEvaluationPossible) return;
241
[14536]242      base.Prepare();
[14538]243      if (clearRuns) runs.Clear();
[14536]244      OnPrepared();
245    }
[14488]246
[14536]247    public override void Start() {
248      if (ensembles == null || datastream == null) return;
249      base.Start();
250      cancellationTokenSource = new CancellationTokenSource();
251      stopPending = false;
252
[14538]253      if (prepared) {
254        SetupResults();
[14543]255        Datastream.InitializeState();
[14538]256      }
257
[14536]258      Task task = Task.Factory.StartNew(Run, cancellationTokenSource.Token, cancellationTokenSource.Token);
259      task.ContinueWith(t => {
260        try {
261          t.Wait();
262        }
263        catch (AggregateException ex) {
264          try {
265            ex.Flatten().Handle(x => x is OperationCanceledException);
266          } catch (AggregateException remaining) {
267            if(remaining.InnerExceptions.Count == 1) OnExceptionOccurred(remaining.InnerExceptions[0]);
268            else OnExceptionOccurred(remaining);
269          }
270        }
271        cancellationTokenSource.Dispose();
272        cancellationTokenSource = null;
273
274        // handle stop/pause
[14538]275        if (stopPending || finished) {
276          OnStopped();
277        } else {
278          OnPaused();
279        }
[14536]280      });
[14488]281    }
282
[14536]283    public override void Pause() {
284      if (ensembles == null || datastream == null) return;
285      base.Pause();
286      cancellationTokenSource.Cancel();
[14488]287    }
288
[14536]289    public override void Stop() {
290      if (ensembles == null || datastream == null) return;
291      base.Stop();
292      if (ExecutionState == ExecutionState.Paused) {
293        OnStopped();
294      } else {
295        stopPending = true;
296        cancellationTokenSource.Cancel();
297      }
[14488]298    }
299
[14536]300    protected override void OnPrepared() {
[14538]301      ExecutionTime = TimeSpan.Zero;
302      foreach (IStatefulItem statefulItem in this.GetObjectGraphObjects(new HashSet<object>() {Runs}).OfType<IStatefulItem>()) {
303        statefulItem.InitializeState();
304      }
305      results.Clear();
306      prepared = true;
307      finished = false;
[14536]308      Log.LogMessage("Datastream analysis prepared");
309      base.OnPrepared();
[14488]310    }
311
[14536]312    protected override void OnStarted() {
313      Log.LogMessage("Datastream analysis started");
314      base.OnStarted();
[14488]315    }
316
[14536]317    protected override void OnPaused() {
318      Log.LogMessage("Datastream analysis paused");
319      base.OnPaused();
320    }
[14488]321
[14536]322    protected override void OnStopped() {
[14538]323      try {
324        runsCounter++;
325        var run = new Run();
326        run.Filename = Filename;
327        run.Name = string.Format("{0} Run {1}", Name, runsCounter);
328        CollectParameterValues(run.Parameters);
329        CollectResultValues(run.Results);
330        runs.Add(run);
331      }
332      finally {
333        Log.LogMessage("Datastream analysis stopped");
334        base.OnStopped();
335      }
[14536]336    }
337
338    #endregion
339
[14538]340    #region run
[14536]341    private void Run(object state) {
342      CancellationToken cancellationToken = (CancellationToken) state;
343      OnStarted();
344      lastUpdateTime = DateTime.UtcNow;
345      System.Timers.Timer timer = new System.Timers.Timer(250);
346      timer.AutoReset = true;
347      timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
348      timer.Start();
349
350      try {
351        Run(cancellationToken);
[14488]352      }
[14536]353      finally {
354        timer.Elapsed -= new System.Timers.ElapsedEventHandler(timer_Elapsed);
355        timer.Stop();
356        ExecutionTime += DateTime.UtcNow - lastUpdateTime;
357      }
358
359      cancellationToken.ThrowIfCancellationRequested();
[14488]360    }
361
[14538]362    private int replayedIndex;
363
[14536]364    protected void Run(CancellationToken cancellationToken) {
[14538]365
366      if (prepared) {
367        replayedIndex = 0;
368        prepared = false;
369      }
370
[14543]371      try {
[14538]372
[14543]373        // play and evaluate initial window
374        PlayDatastream();
375        if(Datastream.SlidingWindowEvaluationPossible) Evaluate();
376        replayedIndex = Datastream.FitnessPartition.End;
[14536]377
[14538]378        do {
[14543]379          while(Datastream.SlidingWindowMovementPossible) {
[14538]380            cancellationToken.ThrowIfCancellationRequested();
[14536]381
[14543]382            // perform (delayed) window movement
[14538]383            Thread.Sleep(Datastream.SlidingWindowMovementInterval.Value);
384            Datastream.MoveSlidingWindow();
[14543]385            ResultsSlidingWindowMovements++;
[14536]386
[14543]387            // play and evaluate the moved window
388            PlayDatastream();
389            if (Datastream.SlidingWindowEvaluationPossible) Evaluate();
[14538]390            replayedIndex = Datastream.FitnessPartition.End;
[14543]391          }
[14538]392        } while (Datastream.UpdateAvailable);
393        finished = true;
[14488]394      }
[14536]395      catch (Exception ex) {
396        if (ex is ArgumentOutOfRangeException) throw ex;
397        if (ex is OperationCanceledException) throw ex;
398      }
399      finally {
400        // reset everything
401        //Prepare(true);
402      }
[14488]403    }
404
[14543]405    private void PlayDatastream() {
406      var problemData = Datastream.ProblemData;
407      var targetVarName = problemData.TargetVariable;
408
409      for (int i = replayedIndex; i < Datastream.FitnessPartition.End; i++) {
410        var realValue = problemData.Dataset.GetDoubleValue(targetVarName, i);
411        ResultsTargets.Rows[ResultsTargetsReal].Values.Add(realValue);
412      }
413    }
414
415    private void Evaluate() {
416      var problemData = Datastream.ProblemData;
417      var dataset = problemData.Dataset;
418      var targetVarName = problemData.TargetVariable;
419
420      var realRows = Enumerable.Range(Datastream.FitnessPartition.Start, Datastream.FitnessPartition.Size).ToList();
421      var realValues = dataset.GetDoubleValues(targetVarName, realRows);
422
423      foreach (var ensemble in Ensembles) {
424
425        var rows = Enumerable.Range(Datastream.FitnessPartition.Start, Datastream.FitnessPartition.Size);
426        var estimatedValuesPerModelPerRow = ensemble.Models.Select(x => x.GetEstimatedValues(datastream.ProblemData.Dataset, rows).ToArray());
427        var estimatedValues = Enumerable.Range(0, Datastream.FitnessPartition.Size).Select(r => estimatedValuesPerModelPerRow.Select(e => e[r]).Average()).ToArray(); // per row
428        var averageEstimatedValuesPerModel = estimatedValuesPerModelPerRow.Select(x => x.Average()); // per model
429        var averageEstimatedValue = averageEstimatedValuesPerModel.Average();
430
431        // determine quality
432        var mse = Math.Pow(averageEstimatedValue - realValues.Average(), 2);
433        OnlineCalculatorError error;
434        var pR = OnlinePearsonsRCalculator.Calculate(estimatedValues, realValues, out error);
435        var pR2 = error == OnlineCalculatorError.None ? pR * pR : 0.0;
436
437        for (int i = replayedIndex; i < Datastream.FitnessPartition.End; i++) {
438          ResultsTargets.Rows[ensemble.Name].Values.Add(averageEstimatedValue);
439
440          //ResultsQualities.Rows[ensemble.Name + " - " + ResultsQualitiesMSE].Values.Add(mse);
441          ResultsQualities.Rows[ensemble.Name + " - " + ResultsQualitiesPR2].Values.Add(pR2);
[14547]442          ResultsQualitiesBars.Bars[new StringValue(ensemble.ItemName)].Value = pR2;
443          ResultsQualitiesBars.Bars[new StringValue(ensemble.ItemName)] = new DoubleValue(pR2);
444          //ResultsQualitiesBars.Bars[new StringValue(ensemble.ItemName)]
[14543]445        }
446      }
447    }
448
[14536]449    private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
450      System.Timers.Timer timer = (System.Timers.Timer)sender;
451      timer.Enabled = false;
452      DateTime now = DateTime.UtcNow;
453      ExecutionTime += now - lastUpdateTime;
454      lastUpdateTime = now;
455      timer.Enabled = true;
456    }
[14488]457
[14538]458    public void CollectParameterValues(IDictionary<string, IItem> values) {
459      values.Add("Datastream Analysis Name", new StringValue(Name));
460      if (Datastream != null) {
461        Datastream.CollectParameterValues(values);
462        values.Add("Datastream Name", new StringValue(Datastream.Name));
463      }
464    }
465
[14536]466    public void CollectResultValues(IDictionary<string, IItem> values) {
467      values.Add("Execution Time", new TimeSpanValue(ExecutionTime));
468      Results.CollectResultValues(values);
[14488]469    }
[14538]470    #endregion
[14488]471
[14536]472    #region events
473
[14538]474    #region events registration
475
[14536]476    public EventHandler EnsemblesChanged;
477    public EventHandler DatastreamChanged;
478
479    protected virtual void DeregisterRunsEvents() {
480      runs.CollectionReset -= new CollectionItemsChangedEventHandler<IRun>(Runs_CollectionReset);
[14488]481    }
482
[14536]483    protected virtual void RegisterRunsEvents() {
484      runs.CollectionReset += new CollectionItemsChangedEventHandler<IRun>(Runs_CollectionReset);
485    }
[14488]486
[14538]487    protected virtual void RegisterDatastreamEvents() {
488      datastream.Reset += new EventHandler(Datastream_Reset);
489      datastream.ProblemDataChanged += new EventHandler(Datastream_ProblemDataChanged);
490    }
491
492    protected virtual void DeregisterDatastreamEvents() {
493      datastream.Reset -= new EventHandler(Datastream_Reset);
494      datastream.ProblemDataChanged -= new EventHandler(Datastream_ProblemDataChanged);
495    }
496
497    protected virtual void RegisterEnsembleEvents() {
498      ensembles.ItemsAdded += new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_ItemsChanged);
499      ensembles.ItemsMoved += new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_ItemsChanged);
500      ensembles.ItemsRemoved += new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_ItemsChanged);
501      ensembles.ItemsReplaced += new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_ItemsChanged);
502      ensembles.CollectionReset += new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_Reset);
503    }
504
505    protected virtual void DeregisterEnsembleEvents() {
506      ensembles.ItemsAdded -= new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_ItemsChanged);
507      ensembles.ItemsMoved -= new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_ItemsChanged);
508      ensembles.ItemsRemoved -= new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_ItemsChanged);
509      ensembles.ItemsReplaced -= new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_ItemsChanged);
510      ensembles.CollectionReset -= new CollectionItemsChangedEventHandler<IndexedItem<RegressionEnsembleModel>>(Ensembles_Reset);
511    }
512    #endregion
513
514    #region event handling
515
[14536]516    protected virtual void Runs_CollectionReset(object sender, CollectionItemsChangedEventArgs<IRun> e) {
517      runsCounter = runs.Count;
518    }
[14488]519
[14538]520    protected virtual void Datastream_Reset(object sender, EventArgs e) {
521      Prepare();
522    }
[14488]523
[14538]524    protected virtual void Datastream_ProblemDataChanged(object sender, EventArgs e) {
525      Prepare();
526    }
527
528    protected virtual void Ensembles_Reset(object sender, EventArgs e) {
529      Prepare();
530    }
531
532    protected virtual void Ensembles_ItemsChanged(object sender, EventArgs e) {
533      Prepare();
534    }
535
[14536]536    private void OnEnsemblesChanged() {
537      var changed = EnsemblesChanged;
538      if (changed != null)
539        changed(this, EventArgs.Empty);
540    }
[14488]541
[14536]542    private void OnDatastreamChanged() {
543      var changed = DatastreamChanged;
544      if (changed != null)
545        changed(this, EventArgs.Empty);
[14488]546    }
547
[14536]548    #endregion
549
550    #endregion
551
[14538]552    #region NamedItem
[14536]553
[14538]554    [Storable]
555    protected string name;
[14536]556
557    /// <inheritdoc/>
558    /// <remarks>Calls <see cref="OnNameChanging"/> and also <see cref="OnNameChanged"/>
559    /// eventually in the setter.</remarks>
560    public string Name {
561      get { return name; }
562      set {
563        if (!CanChangeName) throw new NotSupportedException("Name cannot be changed.");
564        if (!(name.Equals(value) || (value == null) && (name == string.Empty))) {
565          CancelEventArgs<string> e = value == null
566            ? new CancelEventArgs<string>(string.Empty)
567            : new CancelEventArgs<string>(value);
568          OnNameChanging(e);
569          if (!e.Cancel) {
570            name = value == null ? string.Empty : value;
571            OnNameChanged();
572          }
573        }
574      }
[14488]575    }
576
[14536]577    public virtual bool CanChangeName {
578      get { return true; }
[14488]579    }
580
[14536]581    [Storable] protected string description;
582
583    public string Description {
584      get { return description; }
585      set {
586        if (!CanChangeDescription) throw new NotSupportedException("Description cannot be changed.");
587        if (!(description.Equals(value) || (value == null) && (description == string.Empty))) {
588          description = value == null ? string.Empty : value;
589          OnDescriptionChanged();
590        }
591      }
[14488]592    }
593
[14536]594    public virtual bool CanChangeDescription {
595      get { return true; }
[14488]596    }
597
[14536]598    /// <summary>
599    /// Gets the string representation of the current instance in the format: <c>Name: [null|Value]</c>.
600    /// </summary>
601    /// <returns>The current instance as a string.</returns>
602    public override string ToString() {
603      return Name;
[14488]604    }
605
[14536]606    /// <inheritdoc/>
607    public event EventHandler<CancelEventArgs<string>> NameChanging;
608
609    /// <summary>
610    /// Fires a new <c>NameChanging</c> event.
611    /// </summary>
612    /// <param name="e">The event arguments of the changing.</param>
613    protected virtual void OnNameChanging(CancelEventArgs<string> e) {
614      var handler = NameChanging;
615      if (handler != null) handler(this, e);
[14488]616    }
617
[14536]618    /// <inheritdoc/>
619    public event EventHandler NameChanged;
620
621    /// <summary>
622    /// Fires a new <c>NameChanged</c> event.
623    /// </summary>
624    /// <remarks>Calls <see cref="ItemBase.OnChanged"/>.</remarks>
625    protected virtual void OnNameChanged() {
626      var handler = NameChanged;
627      if (handler != null) handler(this, EventArgs.Empty);
628      OnToStringChanged();
[14488]629    }
630
[14536]631    /// <inheritdoc/>
632    public event EventHandler DescriptionChanged;
[14488]633
[14536]634    /// <summary>
635    /// Fires a new <c>DescriptionChanged</c> event.
636    /// </summary>
637    /// <remarks>Calls <see cref="ItemBase.OnChanged"/>.</remarks>
638    protected virtual void OnDescriptionChanged() {
639      var handler = DescriptionChanged;
640      if (handler != null) handler(this, EventArgs.Empty);
641    }
[14488]642
[14536]643    #endregion nameditem
[14488]644  }
[14536]645}
Note: See TracBrowser for help on using the repository browser.