Skip to content

Commit 57ff3f9

Browse files
committed
Established new TopicRelationshipMultiMap from RelatedTopicCollection
The legacy `RelatedTopicCollection` has been renamed to `TopicRelationshipMultiMap` and reconfigured to a) derive from the new `ReadOnlyTopicMultiMap` class (eb5c1c3) in order to provide read-only access to relationships, while b) exposing a façade to a private `TopicMultiMap` object for write access. This ensures that the `TopicRelationshipMultiMap` maintains the semantics of a collection, while enforcing write access through a limited interface which is not only friendlier, but also enforces business logic such as handling reciprocal relationships and state tracking. Speaking of state tracking, the `IsDirty()` status is now tracked internally within the `TopicRelationshipMultiMap` instead of "polluting" the more general downstream objects, such as the `TopicMultiMap` or the `Collection<Topic>` which it relies upon. This is more consistent with how state tracking is (now) handled elsewhere. This is a bit more involved, but helps centralized the tracking functionality. Overall, however, this class is now greatly simplified, since much of the functionality is now handled by the underlying `ReadOnlyTopicMultiMap` or the internal `TopicMultiMap`.
1 parent 9d8ae64 commit 57ff3f9

File tree

1 file changed

+51
-191
lines changed

1 file changed

+51
-191
lines changed

OnTopic/References/RelatedTopicCollection.cs renamed to OnTopic/References/TopicRelationshipMultiMap.cs

Lines changed: 51 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
\=============================================================================================================================*/
66
using System;
77
using System.Collections.Generic;
8-
using System.Collections.ObjectModel;
9-
using System.Linq;
8+
using OnTopic.Collections;
109
using OnTopic.Internal.Diagnostics;
1110

1211
namespace 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

Comments
 (0)