Skip to content

Commit d225a45

Browse files
rafalmaciagclaude
andcommitted
Add XML documentation to all public members and update README
Enable GenerateDocumentationFile, add XML docs to ObservableCollection, ObservableCollectionView<T>, ObservableCollectionView<TDst,TSrc>, IObservableCollectionView, IViewFor, and Extensions. Document BinarySearch/InsertSorted in README. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5591c2d commit d225a45

5 files changed

Lines changed: 276 additions & 2 deletions

File tree

Source/ModelingEvolution.Observable/Extensions.cs

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Collections;
1+
using System.Collections;
22
using System.Collections.ObjectModel;
33
using System.Collections.Specialized;
44
using System.ComponentModel;
@@ -7,6 +7,11 @@
77

88
namespace ModelingEvolution.Observable
99
{
10+
/// <summary>
11+
/// A filtered, observable view over an <see cref="IList{T}"/> that implements <see cref="INotifyCollectionChanged"/>.
12+
/// Setting the <see cref="Filter"/> predicate automatically re-evaluates which items are visible.
13+
/// </summary>
14+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
1015
public class ObservableCollectionView<T> :
1116
INotifyCollectionChanged,
1217
INotifyPropertyChanged,
@@ -24,6 +29,10 @@ public class ObservableCollectionView<T> :
2429
private readonly ObservableCollection<T> _filtered;
2530
private Predicate<T> _filter;
2631

32+
/// <summary>
33+
/// Gets or sets the filter predicate. Setting a new value automatically calls <see cref="Merge"/> to update the view.
34+
/// A <c>null</c> value resets the filter to accept all items.
35+
/// </summary>
2736
public Predicate<T> Filter
2837
{
2938
get => this._filter;
@@ -60,8 +69,16 @@ private void Merge()
6069
this._filtered.RemoveAt(index);
6170
}
6271

72+
/// <summary>
73+
/// Gets the underlying source collection.
74+
/// </summary>
6375
public IList<T> Source => this._internal;
6476

77+
/// <summary>
78+
/// Initializes a new instance of <see cref="ObservableCollectionView{T}"/> with the specified source collection.
79+
/// </summary>
80+
/// <param name="src">The source list to wrap. Must implement <see cref="INotifyCollectionChanged"/>. If <c>null</c>, a new <see cref="ObservableCollection{T}"/> is created.</param>
81+
/// <exception cref="ArgumentException">Thrown when <paramref name="src"/> does not implement <see cref="INotifyCollectionChanged"/>.</exception>
6582
public ObservableCollectionView(IList<T> src = null)
6683
{
6784
this._internal = src ?? (IList<T>)new ObservableCollection<T>();
@@ -119,22 +136,60 @@ private void SourceCollectionChanged(object s, NotifyCollectionChangedEventArgs
119136
}
120137
}
121138

139+
/// <summary>
140+
/// Copies the elements of the collection to an <see cref="Array"/>, starting at a particular index.
141+
/// </summary>
142+
/// <param name="array">The destination array.</param>
143+
/// <param name="index">The zero-based index in <paramref name="array"/> at which copying begins.</param>
122144
public void CopyTo(Array array, int index) => ((ICollection)this._filtered).CopyTo(array, index);
123145

146+
/// <summary>
147+
/// Gets a value indicating whether access to the collection is synchronized (thread safe).
148+
/// </summary>
124149
public bool IsSynchronized => ((ICollection)this._filtered).IsSynchronized;
125150

151+
/// <summary>
152+
/// Gets an object that can be used to synchronize access to the collection.
153+
/// </summary>
126154
public object SyncRoot => ((ICollection)this._filtered).SyncRoot;
127155

156+
/// <summary>
157+
/// Adds an item to the collection.
158+
/// </summary>
159+
/// <param name="value">The object to add.</param>
160+
/// <returns>The position into which the new element was inserted.</returns>
128161
public int Add(object value) => ((IList)this._filtered).Add(value);
129162

163+
/// <summary>
164+
/// Determines whether the collection contains a specific value.
165+
/// </summary>
166+
/// <param name="value">The object to locate.</param>
167+
/// <returns><c>true</c> if <paramref name="value"/> is found; otherwise, <c>false</c>.</returns>
130168
public bool Contains(object value) => ((IList)this._filtered).Contains(value);
131169

170+
/// <summary>
171+
/// Determines the index of a specific value in the collection.
172+
/// </summary>
173+
/// <param name="value">The object to locate.</param>
174+
/// <returns>The index of <paramref name="value"/> if found; otherwise, -1.</returns>
132175
public int IndexOf(object value) => ((IList)this._filtered).IndexOf(value);
133176

177+
/// <summary>
178+
/// Inserts an item at the specified index.
179+
/// </summary>
180+
/// <param name="index">The zero-based index at which <paramref name="value"/> should be inserted.</param>
181+
/// <param name="value">The object to insert.</param>
134182
public void Insert(int index, object value) => ((IList)this._filtered).Insert(index, value);
135183

184+
/// <summary>
185+
/// Removes the first occurrence of a specific object from the collection.
186+
/// </summary>
187+
/// <param name="value">The object to remove.</param>
136188
public void Remove(object value) => ((IList)this._filtered).Remove(value);
137189

190+
/// <summary>
191+
/// Gets a value indicating whether the collection has a fixed size.
192+
/// </summary>
138193
public bool IsFixedSize => ((IList)this._filtered).IsFixedSize;
139194

140195
bool IList.IsReadOnly => false;
@@ -145,55 +200,127 @@ object IList.this[int index]
145200
set => this[index] = (T)value;
146201
}
147202

203+
/// <summary>
204+
/// Adds an item to the filtered collection.
205+
/// </summary>
206+
/// <param name="item">The item to add.</param>
148207
public void Add(T item) => this._filtered.Add(item);
149208

209+
/// <summary>
210+
/// Removes all items from the filtered collection.
211+
/// </summary>
150212
public void Clear() => this._filtered.Clear();
151213

214+
/// <summary>
215+
/// Determines whether the filtered collection contains a specific item.
216+
/// </summary>
217+
/// <param name="item">The item to locate.</param>
218+
/// <returns><c>true</c> if <paramref name="item"/> is found; otherwise, <c>false</c>.</returns>
152219
public bool Contains(T item) => this._filtered.Contains(item);
153220

221+
/// <summary>
222+
/// Copies the elements of the filtered collection to an array, starting at a particular index.
223+
/// </summary>
224+
/// <param name="array">The destination array.</param>
225+
/// <param name="index">The zero-based index in <paramref name="array"/> at which copying begins.</param>
154226
public void CopyTo(T[] array, int index) => this._filtered.CopyTo(array, index);
155227

228+
/// <summary>
229+
/// Returns an enumerator that iterates through the filtered collection.
230+
/// </summary>
231+
/// <returns>An enumerator for the filtered collection.</returns>
156232
public IEnumerator<T> GetEnumerator()
157233
{
158234
foreach (T obj in (Collection<T>)this._filtered)
159235
yield return obj;
160236
}
161237

238+
/// <summary>
239+
/// Determines the index of a specific item in the filtered collection.
240+
/// </summary>
241+
/// <param name="item">The item to locate.</param>
242+
/// <returns>The index of <paramref name="item"/> if found; otherwise, -1.</returns>
162243
public int IndexOf(T item) => this._filtered.IndexOf(item);
163244

245+
/// <summary>
246+
/// Inserts an item at the specified index in the filtered collection.
247+
/// </summary>
248+
/// <param name="index">The zero-based index at which <paramref name="item"/> should be inserted.</param>
249+
/// <param name="item">The item to insert.</param>
164250
public void Insert(int index, T item) => this._filtered.Insert(index, item);
165251

252+
/// <summary>
253+
/// Removes the first occurrence of a specific item from the filtered collection.
254+
/// </summary>
255+
/// <param name="item">The item to remove.</param>
256+
/// <returns><c>true</c> if the item was successfully removed; otherwise, <c>false</c>.</returns>
166257
public bool Remove(T item) => this._filtered.Remove(item);
167258

259+
/// <summary>
260+
/// Removes the item at the specified index from the filtered collection.
261+
/// </summary>
262+
/// <param name="index">The zero-based index of the item to remove.</param>
168263
public void RemoveAt(int index) => this._filtered.RemoveAt(index);
169264

265+
/// <summary>
266+
/// Gets the number of elements in the filtered collection.
267+
/// </summary>
170268
public int Count => this._filtered.Count;
171269

172270
bool ICollection<T>.IsReadOnly => false;
173271

272+
/// <summary>
273+
/// Gets or sets the element at the specified index in the filtered collection.
274+
/// </summary>
275+
/// <param name="index">The zero-based index of the element to get or set.</param>
276+
/// <returns>The element at the specified index.</returns>
174277
public T this[int index]
175278
{
176279
get => this._filtered[index];
177280
set => this._filtered[index] = value;
178281
}
179282

283+
/// <summary>
284+
/// Occurs when a property value changes.
285+
/// </summary>
180286
public event PropertyChangedEventHandler PropertyChanged;
181287

288+
/// <summary>
289+
/// Moves the item at the specified old index to the specified new index.
290+
/// </summary>
291+
/// <param name="oldIndex">The zero-based index of the item to move.</param>
292+
/// <param name="newIndex">The zero-based index to move the item to.</param>
182293
public void Move(int oldIndex, int newIndex) => this._filtered.Move(oldIndex, newIndex);
183294

295+
/// <summary>
296+
/// Occurs when the collection changes.
297+
/// </summary>
184298
public event NotifyCollectionChangedEventHandler CollectionChanged;
185299

186300
IEnumerator IEnumerable.GetEnumerator() => (IEnumerator)this.GetEnumerator();
187301

302+
/// <summary>
303+
/// Unsubscribes from the source collection's change notifications.
304+
/// </summary>
188305
public void Dispose()
189306
{
190307
if (!(this._internal is INotifyCollectionChanged collectionChanged))
191308
return;
192309
collectionChanged.CollectionChanged -= new NotifyCollectionChangedEventHandler(this.SourceCollectionChanged);
193310
}
194311
}
312+
313+
/// <summary>
314+
/// Provides extension methods for collections and string formatting utilities.
315+
/// </summary>
195316
public static class Extensions
196317
{
318+
/// <summary>
319+
/// Adds all elements from <paramref name="other"/> to the end of the list.
320+
/// </summary>
321+
/// <typeparam name="T">The type of elements in the list.</typeparam>
322+
/// <param name="list">The target list to add elements to.</param>
323+
/// <param name="other">The elements to add.</param>
197324
public static void AddRange<T>(this IList<T> list, IEnumerable<T> other)
198325
{
199326
if (list is List<T> objList)
@@ -207,6 +334,12 @@ public static void AddRange<T>(this IList<T> list, IEnumerable<T> other)
207334
}
208335
}
209336

337+
/// <summary>
338+
/// Enumerates the list by index, gracefully stopping on index-out-of-range or concurrent modification errors.
339+
/// </summary>
340+
/// <typeparam name="T">The type of elements in the list.</typeparam>
341+
/// <param name="list">The list to enumerate.</param>
342+
/// <returns>An enumerable sequence of elements from the list.</returns>
210343
public static IEnumerable<T> For<T>(this IReadOnlyList<T> list)
211344
{
212345
for (int i = 0; i < list.Count; i++)
@@ -228,6 +361,13 @@ public static IEnumerable<T> For<T>(this IReadOnlyList<T> list)
228361
}
229362
}
230363

364+
/// <summary>
365+
/// Enumerates the list by index with an optional filter, gracefully stopping on index-out-of-range or concurrent modification errors.
366+
/// </summary>
367+
/// <typeparam name="T">The type of elements in the list.</typeparam>
368+
/// <param name="list">The list to enumerate.</param>
369+
/// <param name="filter">An optional predicate to filter elements. If <c>null</c>, all elements are returned.</param>
370+
/// <returns>An enumerable sequence of elements from the list that match the filter.</returns>
231371
public static IEnumerable<T> For<T>(this IReadOnlyList<T> list, Predicate<T>? filter)
232372
{
233373
for (int i = 0; i < list.Count; i++)
@@ -251,26 +391,55 @@ public static IEnumerable<T> For<T>(this IReadOnlyList<T> list, Predicate<T>? fi
251391
}
252392

253393
private static readonly CultureInfo EN_US = new CultureInfo("en-US");
254-
394+
395+
/// <summary>
396+
/// Returns a debug-friendly representation of the string with escaped newlines and tabs, or "null" if the string is <c>null</c>.
397+
/// </summary>
398+
/// <param name="s">The string to format.</param>
399+
/// <returns>A debug-friendly string representation.</returns>
255400
public static string ToDebugString(this string s)
256401
{
257402
return s == null ? "null" : s.Replace("\n", "\\n").Replace("\t", "\\t");
258403
}
404+
405+
/// <summary>
406+
/// Formats the double value as a JavaScript-compatible string using en-US culture.
407+
/// </summary>
408+
/// <param name="value">The value to format.</param>
409+
/// <returns>The formatted string.</returns>
259410
[MethodImpl(MethodImplOptions.AggressiveInlining)]
260411
public static string AsJs(this double value)
261412
{
262413
return $"{value.ToString(EN_US)}";
263414
}
415+
416+
/// <summary>
417+
/// Formats the integer value as a CSS pixel string (e.g., "42px").
418+
/// </summary>
419+
/// <param name="value">The value to format.</param>
420+
/// <returns>The formatted pixel string.</returns>
264421
[MethodImpl(MethodImplOptions.AggressiveInlining)]
265422
public static string AsPx(this int value)
266423
{
267424
return $"{value}px";
268425
}
426+
427+
/// <summary>
428+
/// Formats the double value as a CSS pixel string using en-US culture (e.g., "3.14px").
429+
/// </summary>
430+
/// <param name="value">The value to format.</param>
431+
/// <returns>The formatted pixel string.</returns>
269432
[MethodImpl(MethodImplOptions.AggressiveInlining)]
270433
public static string AsPx(this double value)
271434
{
272435
return $"{value.ToString(EN_US)}px";
273436
}
437+
438+
/// <summary>
439+
/// Formats the nullable double value as a CSS pixel string using en-US culture, defaulting to 0 if <c>null</c>.
440+
/// </summary>
441+
/// <param name="value">The value to format.</param>
442+
/// <returns>The formatted pixel string.</returns>
274443
[MethodImpl(MethodImplOptions.AggressiveInlining)]
275444
public static string AsPx(this double? value)
276445
{

Source/ModelingEvolution.Observable/IObservableCollectionView.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,25 @@
66

77
namespace ModelingEvolution.Observable;
88

9+
/// <summary>
10+
/// A read-only observable collection view that maps and filters items from <typeparamref name="TSrc"/> to <typeparamref name="TDst"/>.
11+
/// </summary>
12+
/// <typeparam name="TDst">The destination (view-model) item type.</typeparam>
13+
/// <typeparam name="TSrc">The source item type.</typeparam>
914
public interface IObservableCollectionView<TDst, TSrc> :
1015
INotifyCollectionChanged,
1116
INotifyPropertyChanged,
1217
IList<TDst>, IReadOnlyList<TDst>
1318
where TDst : IViewFor<TSrc>, IEquatable<TDst>
1419
{
20+
/// <summary>Gets or sets the element at the specified index.</summary>
1521
new TDst this[int index]
1622
{
1723
get { return ((IReadOnlyList<TDst>)this)[index]; }
1824
set { ((IList<TDst>)this)[index] = value; }
1925
}
2026

27+
/// <summary>Gets the number of elements in the view.</summary>
2128
new int Count
2229
{
2330
get { return ((IReadOnlyList<TDst>)this).Count; }
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
namespace ModelingEvolution.Observable;
22

3+
/// <summary>
4+
/// Marker interface for view-model items that wrap a source item of type <typeparamref name="T"/>.
5+
/// </summary>
6+
/// <typeparam name="T">The type of the underlying source item.</typeparam>
37
public interface IViewFor<out T>
48
{
9+
/// <summary>Gets the underlying source item.</summary>
510
T Source { get; }
611
}

0 commit comments

Comments
 (0)