Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/sources/HeuristicLab.Optimization/3.3/RunCollection.cs @ 11344

Last change on this file since 11344 was 11344, checked in by abeham, 10 years ago

#2120:

  • Parameters and Results are now ObservableDictionaries
  • PropertyChanged event handler replaces the Changed event handler
  • RunCollection listens to changed events to each run's parameters and results (8 additional event handlers per run)
File size: 21.3 KB
RevLine 
[3260]1#region License Information
2/* HeuristicLab
[11171]3 * Copyright (C) 2002-2014 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
[3260]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
[3329]22using System;
[3260]23using System.Collections.Generic;
[11344]24using System.ComponentModel;
[3260]25using System.Linq;
[4068]26using HeuristicLab.Collections;
[3376]27using HeuristicLab.Common;
[3260]28using HeuristicLab.Core;
[3329]29using HeuristicLab.Data;
[3260]30using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
31
32namespace HeuristicLab.Optimization {
[3716]33  [Item("Run Collection", "Represents a collection of runs.")]
34  [Creatable("Testing & Analysis")]
[3260]35  [StorableClass]
[4419]36  public class RunCollection : ItemCollection<IRun>, IStringConvertibleMatrix, IStorableContent {
37    public string Filename { get; set; }
38
[4164]39    [StorableConstructor]
[4888]40    protected RunCollection(bool deserializing)
41      : base(deserializing) {
42      updateOfRunsInProgress = false;
43    }
[4722]44    protected RunCollection(RunCollection original, Cloner cloner)
45      : base(original, cloner) {
[4888]46      updateOfRunsInProgress = false;
[8962]47      optimizerName = original.optimizerName;
[8738]48
[4722]49      resultNames = new List<string>(original.resultNames);
50      parameterNames = new List<string>(original.parameterNames);
51      dataTypes = new Dictionary<string, HashSet<Type>>();
52      foreach (string s in original.dataTypes.Keys)
53        dataTypes[s] = new HashSet<Type>(original.dataTypes[s]);
54
55      constraints = new RunCollectionConstraintCollection(original.constraints.Select(x => cloner.Clone(x)));
[6693]56      modifiers = new CheckedItemList<IRunCollectionModifier>(original.modifiers.Select(cloner.Clone));
[4722]57      foreach (IRunCollectionConstraint constraint in constraints)
58        constraint.ConstrainedValue = this;
59      RegisterConstraintsEvents();
60      RegisterConstraintEvents(constraints);
61
62      UpdateFiltering(true);
63    }
[3329]64    public RunCollection() : base() { Initialize(); }
65    public RunCollection(int capacity) : base(capacity) { Initialize(); }
66    public RunCollection(IEnumerable<IRun> collection) : base(collection) { Initialize(); this.OnItemsAdded(collection); }
[3447]67    private void Initialize() {
[4888]68      updateOfRunsInProgress = false;
[3447]69      parameterNames = new List<string>();
70      resultNames = new List<string>();
[3614]71      dataTypes = new Dictionary<string, HashSet<Type>>();
72      constraints = new RunCollectionConstraintCollection();
[6693]73      modifiers = new CheckedItemList<IRunCollectionModifier>();
[4164]74      RegisterConstraintsEvents();
[3447]75    }
[4164]76
[3625]77    [Storable]
[3614]78    private Dictionary<string, HashSet<Type>> dataTypes;
79    public IEnumerable<Type> GetDataType(string columnName) {
80      if (!dataTypes.ContainsKey(columnName))
81        return new Type[0];
82      return dataTypes[columnName];
83    }
[4164]84
85    [Storable]
[3614]86    private RunCollectionConstraintCollection constraints;
87    public RunCollectionConstraintCollection Constraints {
[3625]88      get { return constraints; }
[3614]89    }
[3329]90
[6693]91    [Storable]
92    private CheckedItemList<IRunCollectionModifier> modifiers;
93    public CheckedItemList<IRunCollectionModifier> Modifiers {
94      get { return modifiers; }
95    }
96
97
[4888]98    private bool updateOfRunsInProgress;
99    public bool UpdateOfRunsInProgress {
100      get { return updateOfRunsInProgress; }
101      set {
102        if (updateOfRunsInProgress != value) {
103          updateOfRunsInProgress = value;
104          OnUpdateOfRunsInProgressChanged();
105        }
106      }
107    }
108
[8962]109    private string optimizerName = string.Empty;
[8738]110    [Storable]
[8962]111    public string OptimizerName {
112      get { return optimizerName; }
[8738]113      set {
[8962]114        if (value != optimizerName && !string.IsNullOrEmpty(value)) {
115          optimizerName = value;
116          OnOptimizerNameChanged();
[8738]117        }
118      }
119    }
120
[8967]121    // BackwardsCompatibility3.3
122    #region Backwards compatible code, remove with 3.4
[8962]123    [Storable(AllowOneWay = true)]
124    private string AlgorithmName {
125      set { optimizerName = value; }
126    }
[8967]127    #endregion
[8962]128
[4888]129    [StorableHook(HookType.AfterDeserialization)]
130    private void AfterDeserialization() {
131      if (constraints == null) constraints = new RunCollectionConstraintCollection();
[6693]132      if (modifiers == null) modifiers = new CheckedItemList<IRunCollectionModifier>();
[4888]133      RegisterConstraintsEvents();
134      RegisterConstraintEvents(constraints);
135      UpdateFiltering(true);
136    }
137
138    public override IDeepCloneable Clone(Cloner cloner) {
139      return new RunCollection(this, cloner);
140    }
141
142    public event EventHandler UpdateOfRunsInProgressChanged;
143    protected virtual void OnUpdateOfRunsInProgressChanged() {
144      var handler = UpdateOfRunsInProgressChanged;
145      if (handler != null) handler(this, EventArgs.Empty);
146    }
147
[8962]148    public event EventHandler OptimizerNameChanged;
149    protected virtual void OnOptimizerNameChanged() {
150      var handler = OptimizerNameChanged;
[8738]151      if (handler != null) handler(this, EventArgs.Empty);
152    }
153
[3329]154    protected override void OnCollectionReset(IEnumerable<IRun> items, IEnumerable<IRun> oldItems) {
155      parameterNames.Clear();
156      resultNames.Clear();
[7798]157      dataTypes.Clear();
[3329]158      foreach (IRun run in items) {
159        foreach (KeyValuePair<string, IItem> parameter in run.Parameters)
160          AddParameter(parameter.Key, parameter.Value);
161        foreach (KeyValuePair<string, IItem> result in run.Results)
162          AddResult(result.Key, result.Value);
[11344]163        run.PropertyChanged += RunOnPropertyChanged;
164        RegisterRunParametersEvents(run);
165        RegisterRunResultsEvents(run);
[3329]166      }
[11344]167      foreach (IRun run in oldItems) {
168        run.PropertyChanged -= RunOnPropertyChanged;
169        DeregisterRunParametersEvents(run);
170        DeregisterRunResultsEvents(run);
171      }
[4200]172      columnNameCache = null;
[5150]173      OnColumnsChanged();
[3329]174      OnColumnNamesChanged();
[4200]175      rowNamesCache = null;
[4518]176      base.OnCollectionReset(items, oldItems);
[5150]177      OnRowsChanged();
[3329]178      OnRowNamesChanged();
[4518]179      OnReset();
180      UpdateFiltering(false);
[3329]181    }
182    protected override void OnItemsAdded(IEnumerable<IRun> items) {
[5150]183      bool columnsChanged = false;
[3329]184      foreach (IRun run in items) {
185        foreach (KeyValuePair<string, IItem> parameter in run.Parameters)
[5150]186          columnsChanged |= AddParameter(parameter.Key, parameter.Value);
[3329]187        foreach (KeyValuePair<string, IItem> result in run.Results)
[5150]188          columnsChanged |= AddResult(result.Key, result.Value);
[11344]189        run.PropertyChanged += RunOnPropertyChanged;
190        RegisterRunParametersEvents(run);
191        RegisterRunResultsEvents(run);
[3329]192      }
[5150]193      if (columnsChanged) columnNameCache = null;
[4200]194      rowNamesCache = null;
[4518]195      base.OnItemsAdded(items);
[4707]196      OnReset();
[5150]197      OnRowsChanged();
[3329]198      OnRowNamesChanged();
[5150]199      if (columnsChanged) {
200        OnColumnsChanged();
201        OnColumnNamesChanged();
202      }
[4518]203      UpdateFiltering(false);
[3329]204    }
205    protected override void OnItemsRemoved(IEnumerable<IRun> items) {
[5150]206      bool columnsChanged = false;
[3329]207      foreach (IRun run in items) {
208        foreach (string parameterName in run.Parameters.Keys)
[5150]209          columnsChanged |= RemoveParameterName(parameterName);
[3329]210        foreach (string resultName in run.Results.Keys)
[5150]211          columnsChanged |= RemoveResultName(resultName);
[11344]212        run.PropertyChanged -= RunOnPropertyChanged;
213        DeregisterRunParametersEvents(run);
214        DeregisterRunResultsEvents(run);
[3329]215      }
[5150]216      if (columnsChanged) columnNameCache = null;
[4200]217      rowNamesCache = null;
[4518]218      base.OnItemsRemoved(items);
[4707]219      OnReset();
[5152]220      OnRowsChanged();
[3329]221      OnRowNamesChanged();
[5150]222      if (columnsChanged) {
223        OnColumnsChanged();
224        OnColumnNamesChanged();
225      }
[3329]226    }
227
[11344]228    private void RunOnPropertyChanged(object sender, PropertyChangedEventArgs e) {
229      if (e.PropertyName == "Parameters") {
230        RegisterRunParametersEvents((IRun)sender);
231      } else if (e.PropertyName == "Results") {
232        RegisterRunResultsEvents((IRun)sender);
233      }
234    }
235
236    private void RegisterRunParametersEvents(IRun run) {
237      IObservableDictionary<string, IItem> dict = run.Parameters;
238      dict.ItemsAdded += RunOnParameterChanged;
239      dict.ItemsRemoved += RunOnParameterChanged;
240      dict.ItemsReplaced += RunOnParameterChanged;
241      dict.CollectionReset += RunOnParameterChanged;
242    }
243
244    private void RegisterRunResultsEvents(IRun run) {
245      IObservableDictionary<string, IItem> dict = run.Results;
246      dict.ItemsAdded += RunOnResultChanged;
247      dict.ItemsRemoved += RunOnResultChanged;
248      dict.ItemsReplaced += RunOnResultChanged;
249      dict.CollectionReset += RunOnResultChanged;
250    }
251
252    private void DeregisterRunParametersEvents(IRun run) {
253      IObservableDictionary<string, IItem> dict = run.Parameters;
254      dict.ItemsAdded -= RunOnParameterChanged;
255      dict.ItemsRemoved -= RunOnParameterChanged;
256      dict.ItemsReplaced -= RunOnParameterChanged;
257      dict.CollectionReset -= RunOnParameterChanged;
258    }
259
260    private void DeregisterRunResultsEvents(IRun run) {
261      IObservableDictionary<string, IItem> dict = run.Results;
262      dict.ItemsAdded -= RunOnResultChanged;
263      dict.ItemsRemoved -= RunOnResultChanged;
264      dict.ItemsReplaced -= RunOnResultChanged;
265      dict.CollectionReset -= RunOnResultChanged;
266    }
267
268    private void RunOnParameterChanged(object sender, CollectionItemsChangedEventArgs<KeyValuePair<string, IItem>> e) {
269      bool columnsChanged = false;
270      foreach (var param in e.Items)
271        columnsChanged |= AddParameter(param.Key, param.Value);
272      foreach (var param in e.OldItems)
273        columnsChanged |= RemoveParameterName(param.Key);
274      if (columnsChanged) columnNameCache = null;
275      OnReset();
276      if (columnsChanged) {
277        OnColumnsChanged();
278        OnColumnNamesChanged();
279      }
280    }
281
282    private void RunOnResultChanged(object sender, CollectionItemsChangedEventArgs<KeyValuePair<string, IItem>> e) {
283      bool columnsChanged = false;
284      foreach (var result in e.Items)
285        columnsChanged |= AddResult(result.Key, result.Value);
286      foreach (var result in e.OldItems)
287        columnsChanged |= RemoveResultName(result.Key);
288      if (columnsChanged) columnNameCache = null;
289      OnReset();
290      if (columnsChanged) {
291        OnColumnsChanged();
292        OnColumnNamesChanged();
293      }
294    }
295
[3329]296    private bool AddParameter(string name, IItem value) {
297      if (value == null)
298        return false;
[3441]299      if (!parameterNames.Contains(name)) {
[3329]300        parameterNames.Add(name);
[3614]301        dataTypes[name] = new HashSet<Type>();
302        dataTypes[name].Add(value.GetType());
[3329]303        return true;
304      }
[3614]305      dataTypes[name].Add(value.GetType());
[3329]306      return false;
307    }
308    private bool AddResult(string name, IItem value) {
309      if (value == null)
310        return false;
[3441]311      if (!resultNames.Contains(name)) {
[3329]312        resultNames.Add(name);
[3614]313        dataTypes[name] = new HashSet<Type>();
314        dataTypes[name].Add(value.GetType());
[3329]315        return true;
316      }
[3614]317      dataTypes[name].Add(value.GetType());
[3329]318      return false;
319    }
320    private bool RemoveParameterName(string name) {
321      if (!list.Any(x => x.Parameters.ContainsKey(name))) {
322        parameterNames.Remove(name);
323        return true;
324      }
325      return false;
326    }
327    private bool RemoveResultName(string name) {
328      if (!list.Any(x => x.Results.ContainsKey(name))) {
329        resultNames.Remove(name);
330        return true;
331      }
332      return false;
333    }
334
[3447]335    public IItem GetValue(int rowIndex, int columnIndex) {
336      IRun run = this.list[rowIndex];
[3492]337      return GetValue(run, columnIndex);
338    }
339
340    public IItem GetValue(IRun run, int columnIndex) {
[3717]341      string name = ((IStringConvertibleMatrix)this).ColumnNames.ElementAt(columnIndex);
342      return GetValue(run, name);
343    }
344
345    public IItem GetValue(IRun run, string columnName) {
[3447]346      IItem value = null;
[3767]347      if (run.Parameters.ContainsKey(columnName))
348        value = run.Parameters[columnName];
349      else if (run.Results.ContainsKey(columnName))
350        value = run.Results[columnName];
[3447]351      return value;
352    }
353
[3329]354    #region IStringConvertibleMatrix Members
[3347]355    [Storable]
[3329]356    private List<string> parameterNames;
[3492]357    public IEnumerable<string> ParameterNames {
358      get { return this.parameterNames; }
359    }
[3347]360    [Storable]
[3329]361    private List<string> resultNames;
[3492]362    public IEnumerable<string> ResultNames {
363      get { return this.resultNames; }
364    }
[3447]365    int IStringConvertibleMatrix.Rows {
[3329]366      get { return this.Count; }
[3447]367      set { throw new NotSupportedException(); }
[3329]368    }
[3447]369    int IStringConvertibleMatrix.Columns {
[3329]370      get { return parameterNames.Count + resultNames.Count; }
371      set { throw new NotSupportedException(); }
372    }
[4200]373    private List<string> columnNameCache;
[3447]374    IEnumerable<string> IStringConvertibleMatrix.ColumnNames {
[3329]375      get {
[4200]376        if (columnNameCache == null) {
377          columnNameCache = new List<string>(parameterNames);
378          columnNameCache.AddRange(resultNames);
379          columnNameCache.Sort();
380        }
381        return columnNameCache;
[3329]382      }
383      set { throw new NotSupportedException(); }
384    }
[4200]385    private List<string> rowNamesCache;
[3447]386    IEnumerable<string> IStringConvertibleMatrix.RowNames {
[4200]387      get {
388        if (rowNamesCache == null)
389          rowNamesCache = list.Select(x => x.Name).ToList();
390        return rowNamesCache;
391      }
[3329]392      set { throw new NotSupportedException(); }
393    }
[3447]394    bool IStringConvertibleMatrix.SortableView {
[3329]395      get { return true; }
396      set { throw new NotSupportedException(); }
397    }
[3447]398    bool IStringConvertibleMatrix.ReadOnly {
399      get { return true; }
[3430]400    }
[3329]401
[3447]402    string IStringConvertibleMatrix.GetValue(int rowIndex, int columnIndex) {
403      IItem value = GetValue(rowIndex, columnIndex);
404      if (value == null)
405        return string.Empty;
406      return value.ToString();
[3329]407    }
408
409    public event EventHandler<EventArgs<int, int>> ItemChanged;
410    protected virtual void OnItemChanged(int rowIndex, int columnIndex) {
[4722]411      EventHandler<EventArgs<int, int>> handler = ItemChanged;
412      if (handler != null) handler(this, new EventArgs<int, int>(rowIndex, columnIndex));
[3329]413      OnToStringChanged();
414    }
415    public event EventHandler Reset;
416    protected virtual void OnReset() {
[4722]417      EventHandler handler = Reset;
418      if (handler != null) handler(this, EventArgs.Empty);
[3329]419      OnToStringChanged();
420    }
[5150]421    public event EventHandler ColumnsChanged;
422    protected virtual void OnColumnsChanged() {
423      var handler = ColumnsChanged;
424      if (handler != null) handler(this, EventArgs.Empty);
425    }
426    public event EventHandler RowsChanged;
427    protected virtual void OnRowsChanged() {
428      var handler = RowsChanged;
429      if (handler != null) handler(this, EventArgs.Empty);
430    }
[3329]431    public event EventHandler ColumnNamesChanged;
432    protected virtual void OnColumnNamesChanged() {
433      EventHandler handler = ColumnNamesChanged;
[4722]434      if (handler != null) handler(this, EventArgs.Empty);
[3329]435    }
436    public event EventHandler RowNamesChanged;
437    protected virtual void OnRowNamesChanged() {
438      EventHandler handler = RowNamesChanged;
[4722]439      if (handler != null) handler(this, EventArgs.Empty);
[3329]440    }
441    public event EventHandler SortableViewChanged;
[3333]442    protected virtual void OnSortableViewChanged() {
443      EventHandler handler = SortableViewChanged;
[4722]444      if (handler != null) handler(this, EventArgs.Empty);
[3333]445    }
446
[3329]447    public bool Validate(string value, out string errorMessage) { throw new NotSupportedException(); }
448    public bool SetValue(string value, int rowIndex, int columnIndex) { throw new NotSupportedException(); }
449    #endregion
[3614]450
[4888]451    #region Filtering
[3632]452    private void UpdateFiltering(bool reset) {
[4888]453      UpdateOfRunsInProgress = true;
[3632]454      if (reset)
455        list.ForEach(r => r.Visible = true);
[3614]456      foreach (IRunCollectionConstraint constraint in this.constraints)
457        constraint.Check();
[4888]458      UpdateOfRunsInProgress = false;
[3614]459    }
460
[4164]461    private void RegisterConstraintsEvents() {
462      constraints.ItemsAdded += new CollectionItemsChangedEventHandler<IRunCollectionConstraint>(Constraints_ItemsAdded);
463      constraints.ItemsRemoved += new CollectionItemsChangedEventHandler<IRunCollectionConstraint>(Constraints_ItemsRemoved);
464      constraints.CollectionReset += new CollectionItemsChangedEventHandler<IRunCollectionConstraint>(Constraints_CollectionReset);
465    }
466
[3614]467    protected virtual void RegisterConstraintEvents(IEnumerable<IRunCollectionConstraint> constraints) {
468      foreach (IRunCollectionConstraint constraint in constraints) {
469        constraint.ActiveChanged += new EventHandler(Constraint_ActiveChanged);
470        constraint.ConstrainedValueChanged += new EventHandler(Constraint_ConstrainedValueChanged);
471        constraint.ConstraintOperationChanged += new EventHandler(Constraint_ConstraintOperationChanged);
472        constraint.ConstraintDataChanged += new EventHandler(Constraint_ConstraintDataChanged);
473      }
474    }
475    protected virtual void DeregisterConstraintEvents(IEnumerable<IRunCollectionConstraint> constraints) {
476      foreach (IRunCollectionConstraint constraint in constraints) {
477        constraint.ActiveChanged -= new EventHandler(Constraint_ActiveChanged);
478        constraint.ConstrainedValueChanged -= new EventHandler(Constraint_ConstrainedValueChanged);
479        constraint.ConstraintOperationChanged -= new EventHandler(Constraint_ConstraintOperationChanged);
480        constraint.ConstraintDataChanged -= new EventHandler(Constraint_ConstraintDataChanged);
481      }
482    }
483
484    protected virtual void Constraints_CollectionReset(object sender, CollectionItemsChangedEventArgs<IRunCollectionConstraint> e) {
485      DeregisterConstraintEvents(e.OldItems);
486      RegisterConstraintEvents(e.Items);
[3632]487      this.UpdateFiltering(true);
[3614]488    }
489    protected virtual void Constraints_ItemsAdded(object sender, CollectionItemsChangedEventArgs<IRunCollectionConstraint> e) {
490      RegisterConstraintEvents(e.Items);
491      foreach (IRunCollectionConstraint constraint in e.Items)
492        constraint.ConstrainedValue = this;
[3632]493      this.UpdateFiltering(false);
[3614]494    }
495    protected virtual void Constraints_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IRunCollectionConstraint> e) {
496      DeregisterConstraintEvents(e.Items);
[3632]497      this.UpdateFiltering(true);
[3614]498    }
499    protected virtual void Constraint_ActiveChanged(object sender, EventArgs e) {
[3632]500      IRunCollectionConstraint constraint = (IRunCollectionConstraint)sender;
501      this.UpdateFiltering(!constraint.Active);
[3614]502    }
503    protected virtual void Constraint_ConstrainedValueChanged(object sender, EventArgs e) {
504      //mkommend: this method is intentionally left empty, because the constrainedValue is set in the ItemsAdded method
505    }
506    protected virtual void Constraint_ConstraintOperationChanged(object sender, EventArgs e) {
[3632]507      IRunCollectionConstraint constraint = (IRunCollectionConstraint)sender;
508      if (constraint.Active)
509        this.UpdateFiltering(true);
[3614]510    }
511    protected virtual void Constraint_ConstraintDataChanged(object sender, EventArgs e) {
[3632]512      IRunCollectionConstraint constraint = (IRunCollectionConstraint)sender;
513      if (constraint.Active)
514        this.UpdateFiltering(true);
[3614]515    }
516    #endregion
[6693]517
518    #region Modification
519    public void Modify() {
520      UpdateOfRunsInProgress = true;
521      var runs = this.ToList();
522      var selectedRuns = runs.Where(r => r.Visible).ToList();
523      int nSelected = selectedRuns.Count;
524      if (nSelected > 0) {
525        foreach (var modifier in Modifiers.CheckedItems)
526          modifier.Value.Modify(selectedRuns);
527        if (nSelected != selectedRuns.Count || HaveDifferentOrder(selectedRuns, runs.Where(r => r.Visible))) {
528          Clear();
529          AddRange(ReplaceVisibleRuns(runs, selectedRuns));
530        } else if (runs.Count > 0) {
531          OnCollectionReset(this, runs);
532        }
533      }
534      UpdateOfRunsInProgress = false;
535    }
536
537    private static IEnumerable<IRun> ReplaceVisibleRuns(IEnumerable<IRun> runs, IEnumerable<IRun> visibleRuns) {
538      var newRuns = new List<IRun>();
539      var runIt = runs.GetEnumerator();
540      var visibleRunIt = visibleRuns.GetEnumerator();
541      while (runIt.MoveNext()) {
542        if (runIt.Current != null && !runIt.Current.Visible)
543          newRuns.Add(runIt.Current);
544        else if (visibleRunIt.MoveNext())
545          newRuns.Add(visibleRunIt.Current);
546      }
547      while (visibleRunIt.MoveNext())
548        newRuns.Add(visibleRunIt.Current);
549      return newRuns;
550    }
551
552    private static bool HaveDifferentOrder(IEnumerable<IRun> l1, IEnumerable<IRun> l2) {
553      return l1.Zip(l2, (r1, r2) => r1 != r2).Any();
554    }
555    #endregion
[3260]556  }
557}
Note: See TracBrowser for help on using the repository browser.