55\=============================================================================================================================*/
66using System ;
77using System . Collections . Generic ;
8- using System . Collections . ObjectModel ;
9- using System . Linq ;
8+ using OnTopic . Collections ;
109using OnTopic . Internal . Diagnostics ;
1110
1211namespace OnTopic . References {
@@ -17,17 +16,15 @@ namespace OnTopic.References {
1716 /// <summary>
1817 /// Provides a simple interface for accessing collections of topic collections.
1918 /// </summary>
20- public class RelatedTopicCollection : KeyedCollection < string , NamedTopicCollection > {
19+ public class RelatedTopicCollection : ReadOnlyTopicMultiMap {
2120
2221 /*==========================================================================================================================
2322 | PRIVATE VARIABLES
2423 \-------------------------------------------------------------------------------------------------------------------------*/
2524 readonly Topic _parent ;
2625 readonly bool _isIncoming ;
27-
28- /*==========================================================================================================================
29- | DATA STORE
30- \-------------------------------------------------------------------------------------------------------------------------*/
26+ readonly List < string > _isDirty = new ( ) ;
27+ readonly TopicMultiMap _storage = new ( ) ;
3128
3229 /*==========================================================================================================================
3330 | CONSTRUCTOR
@@ -42,69 +39,10 @@ public class RelatedTopicCollection : KeyedCollection<string, NamedTopicCollecti
4239 /// set, then it will not allow incoming relationships to be set via the internal <see cref=
4340 /// "SetTopic(String, Topic, Boolean?, Boolean)"/> overload.
4441 /// </remarks>
45- public RelatedTopicCollection ( Topic parent , bool isIncoming = false ) : base ( StringComparer . OrdinalIgnoreCase ) {
46- _parent = parent ;
47- _isIncoming = isIncoming ;
48- }
49-
50- /*==========================================================================================================================
51- | PROPERTY: KEYS
52- \-------------------------------------------------------------------------------------------------------------------------*/
53- /// <summary>
54- /// Retrieves a list of relationship key available.
55- /// </summary>
56- /// <returns>
57- /// Returns an enumerable list of relationship keys.
58- /// </returns>
59- public ReadOnlyCollection < string > Keys => new ( Items . Select ( t => t . Name ) . ToList ( ) ) ;
60-
61- /*==========================================================================================================================
62- | METHOD: GET ALL TOPICS
63- \-------------------------------------------------------------------------------------------------------------------------*/
64- /// <summary>
65- /// Retrieves a list of all related <see cref="Topic"/> objects, independent of relationship key.
66- /// </summary>
67- /// <returns>
68- /// Returns an enumerable list of <see cref="Topic"/> objects.
69- /// </returns>
70- public IEnumerable < Topic > GetAllTopics ( ) {
71- var topics = new List < Topic > ( ) ;
72- foreach ( var topicCollection in this ) {
73- foreach ( var topic in topicCollection ) {
74- if ( ! topics . Contains ( topic ) ) {
75- topics . Add ( topic ) ;
76- }
77- }
78- }
79- return topics ;
80- }
81-
82- /// <summary>
83- /// Retrieves a list of all related <see cref="Topic"/> objects, independent of relationship key, filtered by content
84- /// type.
85- /// </summary>
86- /// <returns>
87- /// Returns an enumerable list of <see cref="Topic"/> objects.
88- /// </returns>
89- public IEnumerable < Topic > GetAllTopics ( string contentType ) => GetAllTopics ( ) . Where ( t => t . ContentType == contentType ) ;
90-
91- /*==========================================================================================================================
92- | METHOD: GET TOPICS
93- \-------------------------------------------------------------------------------------------------------------------------*/
94- /// <summary>
95- /// Retrieves a list of <see cref="Topic"/> objects grouped by a specific relationship key.
96- /// </summary>
97- /// <remarks>
98- /// Returns a reference to the underlying <see cref="NamedTopicCollection"/>; modifications to this collection will modify
99- /// the <see cref="Topic"/>'s <see cref="Topic.Relationships"/>. As such, this should be used with care.
100- /// </remarks>
101- /// <param name="relationshipKey">The key of the relationship to be returned.</param>
102- public NamedTopicCollection GetTopics ( string relationshipKey ) {
103- Contract . Requires < ArgumentNullException > ( ! String . IsNullOrWhiteSpace ( relationshipKey ) , nameof ( relationshipKey ) ) ;
104- if ( Contains ( relationshipKey ) ) {
105- return this [ relationshipKey ] ;
106- }
107- return new ( ) ;
42+ public RelatedTopicCollection ( Topic parent , bool isIncoming = false ) : base ( ) {
43+ _parent = parent ;
44+ _isIncoming = isIncoming ;
45+ base . Source = _storage ;
10846 }
10947
11048 /*==========================================================================================================================
@@ -113,11 +51,19 @@ public NamedTopicCollection GetTopics(string relationshipKey) {
11351 /// <summary>
11452 /// Removes all <see cref="Topic"/> objects grouped by a specific relationship key.
11553 /// </summary>
54+ /// <remarks>
55+ /// If there are any <see cref="Topic"/> objects in the specified <paramref name="relationshipKey"/>, then the <see cref="
56+ /// RelatedTopicCollection"/> will be marked as <see cref="RelatedTopicCollection.IsDirty()"/>.
57+ /// </remarks>
11658 /// <param name="relationshipKey">The key of the relationship to be cleared.</param>
11759 public void ClearTopics ( string relationshipKey ) {
11860 Contract . Requires < ArgumentNullException > ( ! String . IsNullOrWhiteSpace ( relationshipKey ) , nameof ( relationshipKey ) ) ;
119- if ( Contains ( relationshipKey ) ) {
120- this [ relationshipKey ] . Clear ( ) ;
61+ if ( _storage . Contains ( relationshipKey ) ) {
62+ var relationship = _storage . GetTopics ( relationshipKey ) ;
63+ if ( relationship . Count > 0 ) {
64+ MarkDirty ( relationshipKey ) ;
65+ }
66+ _storage . Clear ( relationshipKey ) ;
12167 }
12268 }
12369
@@ -128,62 +74,13 @@ public void ClearTopics(string relationshipKey) {
12874 /// Removes a specific <see cref="Topic"/> object associated with a specific relationship key.
12975 /// </summary>
13076 /// <param name="relationshipKey">The key of the relationship.</param>
131- /// <param name="topicKey">The key of the topic to be removed.</param>
132- /// <returns>
133- /// Returns true if the <see cref="Topic"/> is removed; returns false if either the relationship key or the
134- /// <see cref="Topic"/> cannot be found.
135- /// </returns>
136- public bool RemoveTopic ( string relationshipKey , string topicKey ) => RemoveTopic ( relationshipKey , topicKey , false ) ;
137-
138- /// <summary>
139- /// Removes a specific <see cref="Topic"/> object associated with a specific relationship key.
140- /// </summary>
141- /// <param name="relationshipKey">The key of the relationship.</param>
142- /// <param name="topicKey">The key of the topic to be removed.</param>
143- /// <param name="isIncoming">
144- /// Notes that this is setting an internal relationship, and thus shouldn't set the reciprocal relationship.
145- /// </param>
146- /// <returns>
147- /// Returns true if the <see cref="Topic"/> is removed; returns false if either the relationship key or the
148- /// <see cref="Topic"/> cannot be found.
149- /// </returns>
150- internal bool RemoveTopic ( string relationshipKey , string topicKey , bool isIncoming ) {
151-
152- /*------------------------------------------------------------------------------------------------------------------------
153- | Validate contracts
154- \-----------------------------------------------------------------------------------------------------------------------*/
155- Contract . Requires < ArgumentNullException > ( ! String . IsNullOrWhiteSpace ( relationshipKey ) , nameof ( relationshipKey ) ) ;
156- Contract . Requires < ArgumentNullException > ( ! String . IsNullOrWhiteSpace ( topicKey ) , nameof ( topicKey ) ) ;
157-
158- /*------------------------------------------------------------------------------------------------------------------------
159- | Validate topic key
160- \-----------------------------------------------------------------------------------------------------------------------*/
161- var topics = Contains ( relationshipKey ) ? this [ relationshipKey ] : null ;
162- var topic = topics ? . Contains ( topicKey ) ?? false ? topics [ topicKey ] : null ;
163-
164- if ( topics is null || topic is null ) {
165- return false ;
166- }
167-
168- /*------------------------------------------------------------------------------------------------------------------------
169- | Call overload
170- \-----------------------------------------------------------------------------------------------------------------------*/
171- return RemoveTopic ( relationshipKey , topic , isIncoming ) ;
172-
173- }
174-
175- /// <summary>
176- /// Removes a specific <see cref="Topic"/> object associated with a specific relationship key.
177- /// </summary>
178- /// <param name="relationshipKey">The key of the relationship.</param>
179- /// <param name="topic">The topic to be removed.</param>
77+ /// <param name="topic">The <see cref="Topic"/> to be removed.</param>
18078 /// <returns>
18179 /// Returns true if the <see cref="Topic"/> is removed; returns false if either the relationship key or the
18280 /// <see cref="Topic"/> cannot be found.
18381 /// </returns>
18482 public bool RemoveTopic ( string relationshipKey , Topic topic ) => RemoveTopic ( relationshipKey , topic , false ) ;
18583
186-
18784 /// <summary>
18885 /// Removes a specific <see cref="Topic"/> object associated with a specific relationship key.
18986 /// </summary>
@@ -220,16 +117,15 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming)
220117 /*------------------------------------------------------------------------------------------------------------------------
221118 | Validate relationshipKey
222119 \-----------------------------------------------------------------------------------------------------------------------*/
223- var topics = Contains ( relationshipKey ) ? this [ relationshipKey ] : null ;
224-
225- if ( topics is null || ! topics . Contains ( topic ) ) {
120+ if ( ! _storage . Contains ( relationshipKey , topic ) ) {
226121 return false ;
227122 }
228123
229124 /*------------------------------------------------------------------------------------------------------------------------
230125 | Remove relationship
231126 \-----------------------------------------------------------------------------------------------------------------------*/
232- topics . Remove ( topic ) ;
127+ MarkDirty ( relationshipKey ) ;
128+ _storage . Remove ( relationshipKey , topic ) ;
233129
234130 /*------------------------------------------------------------------------------------------------------------------------
235131 | Remove true
@@ -250,7 +146,7 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming)
250146 /// <param name="relationshipKey">The key of the relationship.</param>
251147 /// <param name="topic">The topic to be added, if it doesn't already exist.</param>
252148 /// <param name="isDirty">
253- /// Optionally forces the collection to a <see cref="NamedTopicCollection. IsDirty"/> state, assuming the topic was set.
149+ /// Optionally forces the collection to an <see cref="IsDirty() "/> state, assuming the topic was set.
254150 /// </param>
255151 public void SetTopic ( string relationshipKey , Topic topic , bool ? isDirty = null )
256152 => SetTopic ( relationshipKey , topic , isDirty , false ) ;
@@ -267,7 +163,7 @@ public void SetTopic(string relationshipKey, Topic topic, bool? isDirty = null)
267163 /// Notes that this is setting an internal relationship, and thus shouldn't set the reciprocal relationship.
268164 /// </param>
269165 /// <param name="isDirty">
270- /// Optionally forces the collection to a <see cref="NamedTopicCollection. IsDirty"/> state, assuming the topic was set.
166+ /// Optionally forces the collection to an <see cref="IsDirty() "/> state, assuming the topic was set.
271167 /// </param>
272168 internal void SetTopic ( string relationshipKey , Topic topic , bool ? isDirty , bool isIncoming ) {
273169
@@ -281,14 +177,15 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool
281177 /*------------------------------------------------------------------------------------------------------------------------
282178 | Add relationship
283179 \-----------------------------------------------------------------------------------------------------------------------*/
284- if ( ! Contains ( relationshipKey ) ) {
285- Add ( new ( relationshipKey ) ) ;
286- }
287- var topics = this [ relationshipKey ] ;
288- if ( ! topics . Contains ( topic . Key ) ) {
289- topics . Add ( topic ) ;
290- if ( ! ( isDirty ?? topics . IsDirty ( ) ) ) {
291- topics . MarkClean ( ) ;
180+ var topics = _storage . GetTopics ( relationshipKey ) ;
181+ var wasDirty = _isDirty . Contains ( relationshipKey ) ;
182+ if ( ! topics . Contains ( topic ) ) {
183+ _storage . Add ( relationshipKey , topic ) ;
184+ if ( isDirty . HasValue && ! isDirty . Value && ! wasDirty ) {
185+ MarkClean ( relationshipKey ) ;
186+ }
187+ else {
188+ MarkDirty ( relationshipKey ) ;
292189 }
293190 }
294191
@@ -311,75 +208,38 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool
311208 | METHOD: IS DIRTY?
312209 \-------------------------------------------------------------------------------------------------------------------------*/
313210 /// <summary>
314- /// Evaluates each of the child <see cref="NamedTopicCollection"/>s to determine if any of them are set to <see
315- /// cref="NamedTopicCollection.IsDirty"/>. If they are, returns <c>true</c>.
211+ /// Determines if any of the relationships have been modified; if they have, returns <c>true</c>.
316212 /// </summary>
317- public bool IsDirty ( ) => Items . Any ( r => r . IsDirty ( ) ) ;
213+ public bool IsDirty ( ) => _isDirty . Count > 0 ;
318214
319215 /*==========================================================================================================================
320- | METHOD: MARK CLEAN
216+ | METHOD: MARK DIRTY
321217 \-------------------------------------------------------------------------------------------------------------------------*/
322218 /// <summary>
323- /// Sets the <see cref="NamedTopicCollection.IsDirty"/> property of every <see cref="NamedTopicCollection "/> in this <see
324- /// cref="RelatedTopicCollection"/> to <c>false </c>.
219+ /// Evaluates each of the relationships to determine if any of them are set to <see cref="IsDirty() "/>. If they are,
220+ /// returns <c>true </c>.
325221 /// </summary>
326- public void MarkClean ( ) {
327- foreach ( var relationship in Items ) {
328- relationship . MarkClean ( ) ;
222+ private void MarkDirty ( string relationshipKey ) {
223+ if ( ! _isDirty . Contains ( relationshipKey ) ) {
224+ _isDirty . Add ( relationshipKey ) ;
329225 }
330226 }
331227
332228 /*==========================================================================================================================
333- | OVERRIDE: INSERT ITEM
229+ | METHOD: MARK CLEAN
334230 \-------------------------------------------------------------------------------------------------------------------------*/
335- /// <summary>Fires any time a <see cref="NamedTopicCollection"/> is added to the collection.</summary>
336- /// <remarks>
337- /// Compared to the base implementation, will throw a specific <see cref="ArgumentException"/> error if a duplicate key is
338- /// inserted. This conveniently provides the name of the <see cref="NamedTopicCollection"/>, so it's clear what key is
339- /// being duplicated.
340- /// </remarks>
341- /// <param name="index">The zero-based index at which <paramref name="item" /> should be inserted.</param>
342- /// <param name="item">The <see cref="NamedTopicCollection"/> instance to insert.</param>
343- /// <exception cref="ArgumentException">
344- /// A NamedTopicCollection with the Name '{item.Name}' already exists in this RelatedTopicCollection. The existing key is
345- /// {this[item.Name].Name}'; the new item's is '{item.Name}'. This collection is associated with the '{GetUniqueKey()}'
346- /// Topic.
347- /// </exception>
348- protected override void InsertItem ( int index , NamedTopicCollection item ) {
349-
350- /*------------------------------------------------------------------------------------------------------------------------
351- | Validate parameters
352- \-----------------------------------------------------------------------------------------------------------------------*/
353- Contract . Requires ( item , nameof ( item ) ) ;
354-
355- /*------------------------------------------------------------------------------------------------------------------------
356- | Insert item, if it doesn't already exist
357- \-----------------------------------------------------------------------------------------------------------------------*/
358- if ( ! Contains ( item . Name ) ) {
359- base . InsertItem ( index , item ) ;
360- }
361- else {
362- throw new ArgumentException (
363- $ "A { nameof ( NamedTopicCollection ) } with the Name '{ item . Name } ' already exists in this " +
364- $ "{ nameof ( RelatedTopicCollection ) } . The existing key is '{ this [ item . Name ] . Name } '; the new item's is '{ item . Name } '. " +
365- $ "This collection is associated with the '{ _parent . GetUniqueKey ( ) } ' Topic.",
366- nameof ( item )
367- ) ;
368- }
369- }
231+ /// <summary>
232+ /// Marks the relationships collections as clean.
233+ /// </summary>
234+ public void MarkClean ( ) => _isDirty . Clear ( ) ;
370235
371- /*==========================================================================================================================
372- | OVERRIDE: GET KEY FOR ITEM
373- \-------------------------------------------------------------------------------------------------------------------------*/
374236 /// <summary>
375- /// Provides a method for the <see cref="KeyedCollection{TKey, TItem}"/> to retrieve the key from the underlying
376- /// collection of objects, in this case <see cref="NamedTopicCollection"/>s.
237+ /// Removes the <paramref name="relationshipKey"/> from the <see cref="_isDirty"/> collection, if it exists.
377238 /// </summary>
378- /// <param name="item">The <see cref="Topic"/> object from which to extract the key.</param>
379- /// <returns>The key for the specified collection item.</returns>
380- protected override string GetKeyForItem ( NamedTopicCollection item ) {
381- Contract . Requires ( item , "The item must be available in order to derive its key." ) ;
382- return item . Name ;
239+ public void MarkClean ( string relationshipKey ) {
240+ if ( _isDirty . Contains ( relationshipKey ) ) {
241+ _isDirty . Remove ( relationshipKey ) ;
242+ }
383243 }
384244
385245 } //Class
0 commit comments