# SignalR DataSource Change-tracked real-time collection built on top of the SignalR transport layer. Source: `AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs`. > For the underlying transport (tag system, wire protocol, dispatch) see [`SIGNALR.md`](SIGNALR.md). ## Overview `AcSignalRDataSource` is a generic `IList` that synchronizes with the server via CRUD tags. It handles change tracking, rollback, sync state, and binary deserialization — so consuming code works with a regular list while the DataSource manages communication. ```csharp AcSignalRDataSource where TDataItem : class, IId // entity with ID where TId : struct // Guid, int, etc. where TIList : class, IList // List or AcObservableCollection ``` Implements `IList`, `IList`, `IReadOnlyList`. **Constructor:** ```csharp new AcSignalRDataSource( AcSignalRClientBase signalRClient, // transport client SignalRCrudTags crudTags, // 5 tags for CRUD operations object[]? contextIds = null // optional server-side filter context ) ``` ## SignalRCrudTags A `sealed class` that bundles **5 independent tag integers** — one per CRUD operation. Tags are NOT sequential offsets; each tag is independently assigned: ```csharp public sealed class SignalRCrudTags( int getAllTag, int getItemTag, int addTag, int updateTag, int removeTag) ``` **Usage (consuming project):** ```csharp // Tags are defined as independent constants in the project's tag class public abstract class MyProjectTags : AcSignalRTags { public const int OrderGetAll = 300; public const int OrderGetItem = 301; public const int OrderAdd = 302; public const int OrderUpdate = 303; public const int OrderRemove = 304; // Tags don't have to be sequential — they just happen to be here } // Construct with 5 explicit tags var crudTags = new SignalRCrudTags( MyProjectTags.OrderGetAll, MyProjectTags.OrderGetItem, MyProjectTags.OrderAdd, MyProjectTags.OrderUpdate, MyProjectTags.OrderRemove ); ``` **Tag lookup:** `GetMessageTagByTrackingState(TrackingState)` maps tracking state to the corresponding tag via switch expression. ## Data Loading ```csharp // Awaits full response via sync-wait transport pattern (polls up to 60s) await dataSource.LoadDataSource(); // Async callback — requests SignalResponseDataMessage directly to avoid double deserialization await dataSource.LoadDataSourceAsync(); // Load from raw response bytes (public — usable when response is already available) await dataSource.LoadDataSourceFromResponseData(responseData, serializerType); // Single item by ID — updates or adds to InnerList await dataSource.LoadItem(id); ``` All load methods are `async Task`. `LoadDataSource` uses the sync-wait transport pattern (`GetAllAsync` → polls for response), while `LoadDataSourceAsync` uses the async callback path (avoids deserializing `ResponseData` into a typed object — works directly with `byte[]`). **Binary deserialization paths:** - `AcObservableCollection`: `BeginUpdate()` → `BinaryToMerge()` → `EndUpdate()` — single batched UI notification. - `List`: `BinaryTo(InnerList)` — direct populate. **Context/Filtering:** `ContextIds` (object[]) and `FilterText` (string) are sent with every GetAll request for server-side filtering. ## Change Tracking Each modified item is wrapped in `TrackingItem`: | Field | Purpose | |-------|---------| | `TrackingState` | Add, Update, Remove | | `CurrentValue` | Current item reference | | `OriginalValue` | Clone for rollback (`JsonClone` or `ReflectionClone`) | The `ChangeTracking` class manages the list of tracked items. ### CRUD Operations ``` Add(item): → TrackingState.Add + InnerList.Add(item) AddOrUpdate(item): → exists? Update : Add (determines TrackingState automatically) Insert(index, item): → TrackingState.Add + InnerList.Insert(index, item) Update(i, item): → TrackingState.Update + clone original + InnerList[i] = item Remove(id): → TrackingState.Remove + clone original + InnerList.RemoveAt(index) ``` Each operation has an optional `autoSave` parameter — if true, immediately calls `SaveItem()` for that single change. **Manual tracking:** `SetTrackingStateToUpdate(item)` marks an existing item as modified without replacing it — useful when properties are mutated in-place. ### Events | Event | Signature | Fires when | |-------|-----------|------------| | `OnDataSourceItemChanged` | `Func, Task>?` | After each item is saved or loaded (carries item + TrackingState) | | `OnDataSourceLoaded` | `Func?` | After `LoadDataSource` / `LoadDataSourceAsync` completes | | `OnSyncingStateChanged` | `Action?` | On 0→1 (true) and 1→0 (false) sync transitions | ## SaveChanges Two variants with different transport patterns: | Method | Returns | Transport pattern | Use case | |--------|---------|-------------------|----------| | `SaveChanges()` | `List` (remaining failures) | Sync-wait (`ContinueWith`) | When caller needs to know what failed | | `SaveChangesAsync()` | `Task` (void) | Fire-and-forget callback (`Action`) | Background save, no result inspection | ``` Both follow the same flow: BeginSync() for each tracked item: tag = CrudTags.GetMessageTagByTrackingState(state) response = SignalRClient.PostDataAsync(tag, item) on success: remove from tracking, CopyTo InnerList item with server response on failure: TryRollbackItem() → restore OriginalValue EndSync() ``` **SaveItem(item, trackingState):** saves a single item (same flow). **SaveItem(id):** looks up tracking item by ID, then saves. **Rollback:** `TryRollbackItem(id)` restores `OriginalValue` to `InnerList`. For `TrackingState.Add`: removes item entirely. For `Remove`: re-adds `OriginalValue`. Manual `Rollback()` reverts all tracked changes at once. ## Sync State ```csharp private int _activeSyncOperations; // Interlocked counter BeginSync(): Interlocked.Increment → fires OnSyncingStateChanged(true) on 0→1 EndSync(): Interlocked.Decrement → fires OnSyncingStateChanged(false) on 1→0 IsSyncing: _activeSyncOperations > 0 ``` UI binds to `IsSyncing` to show loading indicators. The counter supports nested sync operations. ## Working Reference List Allows an external list to become the DataSource's inner storage: ```csharp dataSource.SetWorkingReferenceList(externalList); // Now dataSource operates directly on externalList — same reference, no copy // Read back the current inner list reference TIList innerList = dataSource.GetReferenceInnerList(); ``` This is useful when the UI already has a bound collection and you want the DataSource to manage it in-place. `HasWorkingReferenceList` indicates whether an external list has been set. ## Locking Strategy | Lock | Scope | Used by | |------|-------|---------| | `object _syncRoot` | Synchronous | Count, Contains, IndexOf, GetEnumerator | | `SemaphoreSlim _asyncLock` | Asynchronous | Add/Update/Remove with save, LoadDataSource | `GetEnumerator()` returns `InnerList.ToList().GetEnumerator()` — safe copy to avoid mutation during iteration. ## Relationship to Transport The DataSource is a **consumer** of the SignalR transport, not part of it. The flow: ``` DataSource.SaveChanges() → CrudTags.GetMessageTagByTrackingState(state) → tag → AcSignalRClientBase.PostDataAsync(tag, item) ← transport layer → OnReceiveMessage(tag, bytes, requestId) ← wire protocol → Server method with [SignalR(tag)] ← tag dispatch ``` Projects can also call the transport directly without DataSource — see [`SIGNALR.md` § How Projects Use Tags](SIGNALR.md#how-projects-use-tags). ## Key Source Files | Component | Path | |-----------|------| | DataSource | `AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs` | | CRUD tags | `AyCode.Services/SignalRs/SignalRCrudTags.cs` | | Tracking helpers | `AyCode.Services.Server/SignalRs/TrackingItemHelpers.cs` | | Transport client | `AyCode.Services/SignalRs/AcSignalRClientBase.cs` | | Transport doc | [`docs/SIGNALR.md`](SIGNALR.md) |