# SignalR DataSource Change-tracked real-time collection built on top of the SignalR transport layer. Source: `SignalRs/AcSignalRDataSource.cs` in this project. > For the underlying transport (tag system, wire protocol, client base) see [`AyCode.Services/docs/SIGNALR.md`](../../AyCode.Services/docs/SIGNALR.md). > For server hub infrastructure see [`SIGNALR_SERVER.md`](SIGNALR_SERVER.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 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; } 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 await dataSource.LoadDataSource(); // sync-wait transport (polls up to 60s) await dataSource.LoadDataSourceAsync(); // async callback path await dataSource.LoadDataSourceFromResponseData(responseData, serializerType); await dataSource.LoadItem(id); // single item by ID ``` **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 | | `OnDataSourceLoaded` | `Func?` | After `LoadDataSource` / `LoadDataSourceAsync` completes | | `OnSyncingStateChanged` | `Action?` | On 0→1 (true) and 1→0 (false) sync transitions | ## SaveChanges | 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() ``` **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 ```csharp dataSource.SetWorkingReferenceList(externalList); // Now dataSource operates directly on externalList — same reference, no copy TIList innerList = dataSource.GetReferenceInnerList(); ``` Useful when the UI already has a bound collection and you want the DataSource to manage it in-place. ## 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: ``` 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 [`AyCode.Services/docs/SIGNALR.md`](../../AyCode.Services/docs/SIGNALR.md). ## Key Source Files | Component | Path | |-----------|------| | DataSource | `SignalRs/AcSignalRDataSource.cs` | | Tracking helpers | `SignalRs/TrackingItemHelpers.cs` | | CRUD tags | `AyCode.Services/SignalRs/SignalRCrudTags.cs` | | Transport client | `AyCode.Services/SignalRs/AcSignalRClientBase.cs` |