192 lines
7.3 KiB
Markdown
192 lines
7.3 KiB
Markdown
# 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`.
|
|
> For server hub infrastructure see `SIGNALR_SERVER.md`.
|
|
|
|
## Overview
|
|
|
|
`AcSignalRDataSource` is a generic `IList<T>` 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<TDataItem, TId, TIList>
|
|
where TDataItem : class, IId<TId> // entity with ID
|
|
where TId : struct // Guid, int, etc.
|
|
where TIList : class, IList<TDataItem> // List<T> or AcObservableCollection<T>
|
|
```
|
|
|
|
Implements `IList<TDataItem>`, `IList`, `IReadOnlyList<TDataItem>`.
|
|
|
|
**Constructor:**
|
|
|
|
```csharp
|
|
new AcSignalRDataSource<T, TId, TIList>(
|
|
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
|
|
```
|
|
|
|
**Deserialization paths:**
|
|
- **Typed response** (`T != byte[]`): protocol eagerly deserializes via `SignalDataType` → `GetResponseData<T>()` direct cast.
|
|
- **Raw byte[] response** (`IsRawBytesData`): protocol returns raw `byte[]` → consumer deserializes:
|
|
- `AcObservableCollection<T>`: `BeginUpdate()` → `PopulateMerge(bytes)` → `EndUpdate()` — single batched UI notification.
|
|
- `List<T>`: `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<TDataItem, TId>`:
|
|
|
|
| Field | Purpose |
|
|
|-------|---------|
|
|
| `TrackingState` | Add, Update, Remove |
|
|
| `CurrentValue` | Current item reference |
|
|
| `OriginalValue` | Clone for rollback (`JsonClone` or `ReflectionClone`) |
|
|
|
|
The `ChangeTracking<TDataItem, TId>` 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<ItemChangedEventArgs<T>, Task>?` | After each item is saved or loaded |
|
|
| `OnDataSourceLoaded` | `Func<Task>?` | After `LoadDataSource` / `LoadDataSourceAsync` completes |
|
|
| `OnSyncingStateChanged` | `Action<bool>?` | On 0→1 (true) and 1→0 (false) sync transitions |
|
|
|
|
## SaveChanges
|
|
|
|
| Method | Returns | Transport pattern | Use case |
|
|
|--------|---------|-------------------|----------|
|
|
| `SaveChanges()` | `List<TrackingItem>` (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`.
|
|
|
|
## 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` |
|