-
Notifications
You must be signed in to change notification settings - Fork 25
/
Grijjy.Mvvm.Observable.pas
706 lines (577 loc) · 20.7 KB
/
Grijjy.Mvvm.Observable.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
unit Grijjy.Mvvm.Observable;
{$INCLUDE 'Grijjy.inc'}
interface
uses
System.Types,
System.Generics.Defaults,
System.Generics.Collections,
Grijjy.System,
Grijjy.Mvvm.Types;
type
{ Abstract base class for classes with observable properties.
That is, other classes can observe classes derived from TgoObservable for
changes in its properties.
Classes derived from this type can be sources (and targets) for data
binding.
To be the source of a data binding, the class must call PropertyChanged
whenever a property that is the source of a data binding has changed. Care
must be taken to only call this method when the property value has actually
changed (using the normal "if (Value <> SomeProp) then" pattern), and
AFTER the value has changed.
This class also implements IgoNotifyFree so it sends a free notification
when it is about to be destroyed.
NOTE: This class implements interfaces but is not reference counted. }
TgoObservable = class abstract(TgoNonRefCountedObject, IgoNotifyPropertyChanged, IgoNotifyFree)
{$REGION 'Internal Declarations'}
private
FOnPropertyChanged: IgoPropertyChangedEvent;
FOnFree: IgoFreeEvent;
protected
{ IgoNotifyPropertyChanged }
function GetPropertyChangedEvent: IgoPropertyChangedEvent;
protected
{ IgoNotifyFree }
function GetFreeEvent: IgoFreeEvent;
{$ENDREGION 'Internal Declarations'}
protected
{ A derived class should call this method every time a property that is
the source of a data binding has changed.
Parameters:
APropertyName: the name of the property that has changed. This name
is case sensitive and must exactly match the name of the property.
Care must be taken to only call this method when the property value has
actually changed (using the normal "if (Value <> SomeProp) then"
pattern), and AFTER the value has changed. }
procedure PropertyChanged(const APropertyName: String);
public
{ The destructor also sends a free notification to its subscribers. }
destructor Destroy; override;
end;
type
{ A dynamic data collection that provides notifications (through
IgoNotifyCollectionChanged) when the collection or items in the collection
change.
To support notifications of changes to individual items, the type parameter
T must implement the IgoPropertyChangedEvent interface.
The collection itself supports property changed notifications for: Count }
TgoObservableCollection<T: class> = class(TEnumerable<T>,
IgoNotifyPropertyChanged, IgoNotifyCollectionChanged)
{$REGION 'Internal Declarations'}
private
FList: TObjectList<T>;
FOnPropertyChanged: IgoPropertyChangedEvent;
FOnCollectionChanged: IgoCollectionChangedEvent;
function GetCapacity: Integer; inline;
function GetCount: Integer; inline;
function GetItem(const AIndex: Integer): T; inline;
procedure SetItem(const AIndex: Integer; const Value: T); inline;
private
procedure DoItemPropertyChanged(const ASender: TObject;
const APropertyName: String);
procedure DoPropertyChanged(const APropertyName: String);
procedure DoCollectionChanged(const AAction: TgoCollectionChangedAction;
const AItem: TObject = nil; const AItemIndex: Integer = -1;
const APropertyName: String = '');
protected
{ IInterface }
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
protected
{ IgoNotifyPropertyChanged }
function GetPropertyChangedEvent: IgoPropertyChangedEvent;
protected
{ IgoNotifyCollectionChanged }
function GetCollectionChangedEvent: IgoCollectionChangedEvent;
{$ENDREGION 'Internal Declarations'}
public
constructor Create(const AOwnsObjects: Boolean = False);
destructor Destroy; override;
public
{ TEnumerable<T> }
{ Copies the elements in the collection to a dynamic array }
function ToArray: TArray<T>; override; final;
{ Allow <tt>for..in</tt> enumeration of the collection. }
function DoGetEnumerator: TEnumerator<T>; override;
public
{ Checks whether the collection contains a given item.
This method performs a O(n) linear search and uses the collection's
comparer to check for equality. For a faster check, use BinarySearch.
Parameters:
AItem: The item to check.
Returns:
True if the collection contains AValue. }
function Contains(const AItem: T): Boolean; inline;
{ Returns the index of a given item or -1 if not found.
This method performs a O(n) linear search and uses the collection's
comparer to check for equality. For a faster check, use BinarySearch.
Parameters:
AItem: The item to find. }
function IndexOf(const AItem: T): Integer; inline;
{ Returns the last index of a given item or -1 if not found.
This method performs a O(n) backwards linear search and uses the
collection's comparer to check for equality. For a faster check, use
BinarySearch.
Parameters:
AItem: The item to find. }
function LastIndexOf(const AItem: T): Integer; inline;
{ Returns the index of a given item or -1 if not found.
This method performs a O(n) linear search and uses the collection's
comparer to check for equality. For a faster check, use BinarySearch.
Parameters:
AItem: The item to find.
ADirection: Whether to search forwards or backwards. }
function IndexOfItem(const AItem: T; const ADirection: TDirection): Integer; inline;
{ Performs a binary search for a given item. This requires that the
collection is sorted. This is an O(log n) operation that uses the default
comparer to check for equality.
Parameters:
AItem: The item to find.
AIndex: is set to the index of AItem if found. If not found, it is set
to the index of the first entry larger than AItem.
Returns:
Whether the collection contains the item. }
function BinarySearch(const AItem: T; out AIndex: Integer): Boolean; overload; inline;
{ Performs a binary search for a given item. This requires that the
collection is sorted. This is an O(log n) operation that uses the given
comparer to check for equality.
Parameters:
AItem: The item to find.
AIndex: is set to the index of AItem if found. If not found, it is set
to the index of the first entry larger than AItem.
AComparer: the comparer to use to check for equality.
Returns:
Whether the collection contains the item. }
function BinarySearch(const AItem: T; out AIndex: Integer;
const AComparer: IComparer<T>): Boolean; overload; inline;
{ Returns the first item in the collection. }
function First: T; inline;
{ Returns the last item in the collection. }
function Last: T; inline;
{ Clears the collection }
procedure Clear;
{ Adds an item to the end of the collection.
Parameters:
AItem: the item to add.
Returns:
The index of the added item. }
function Add(const AItem: T): Integer;
{ Adds a range of items to the end of the collection.
Parameters:
AItems: an array of items to add. }
procedure AddRange(const AItems: array of T); overload;
{ Adds the items of another collection to the end of the collection.
Parameters:
ACollection: the collection containing the items to add. Can be any
class that descends from TEnumerable<T>. }
procedure AddRange(const ACollection: TEnumerable<T>); overload;
{ Inserts an item into the collection.
Parameters:
AIndex: the index in the collection to insert the item. The item will be
inserted before AIndex. Set to 0 to insert at the beginning to the
collection. Set to Count to add to the end of the collection.
AItem: the item to insert. }
procedure Insert(const AIndex: Integer; const AItem: T);
{ Inserts a range of items into the collection.
Parameters:
AIndex: the index in the collection to insert the items. The items will
be inserted before AIndex. Set to 0 to insert at the beginning to the
collection. Set to Count to add to the end of the collection.
AItems: the items to insert. }
procedure InsertRange(const AIndex: Integer; const AItems: array of T); overload;
{ Inserts the items from another collection into the collection.
Parameters:
AIndex: the index in the collection to insert the items. The items will
be inserted before AIndex. Set to 0 to insert at the beginning to the
collection. Set to Count to add to the end of the collection.
ACollection: the collection containing the items to insert. Can be any
class that descends from TEnumerable<T>. }
procedure InsertRange(const AIndex: Integer; const ACollection: TEnumerable<T>); overload;
{ Deletes an item from the collection.
Parameters:
AIndex: the index of the item to delete }
procedure Delete(const AIndex: Integer);
{ Deletes a range of items from the collection.
Parameters:
AIndex: the index of the first item to delete
ACount: the number of items to delete }
procedure DeleteRange(const AIndex, ACount: Integer);
{ Removes an item from the collection.
Parameters:
AItem: the item to remove. It this collection does not contain this
item, nothing happens.
Returns:
The index of the removed item, or -1 of the collection does not contain
AItem.
If the collection contains multiple items with the same value, only the
first item is removed. }
function Remove(const AItem: T): Integer;
{ Reverses the order of the elements in the collection. }
procedure Reverse;
{ Sort the collection using the default comparer for the element type }
procedure Sort; overload;
{ Sort the collection using a custom comparer.
Parameters:
AComparer: the comparer to use to sort the collection. }
procedure Sort(const AComparer: IComparer<T>); overload;
{ Trims excess memory used by the collection. To improve performance and
reduce memory reallocations, the collection usually contains space for
more items than are actually stored in this collection. That is,
Capacity >= Count. Call this method free that excess memory. You can do
this when you are done filling the collection to free memory. }
procedure TrimExcess; inline;
{ The number of items in the collection }
property Count: Integer read GetCount;
{ The items in the collection }
property Items[const AIndex: Integer]: T read GetItem write SetItem; default;
{ The number of reserved items in the collection. Is >= Count to improve
performance by reducing memory reallocations. }
property Capacity: Integer read GetCapacity;
end;
implementation
uses
System.SysUtils;
{ TgoObservable }
destructor TgoObservable.Destroy;
begin
if Assigned(FOnFree) then
FOnFree.Invoke(Self, Self);
inherited;
end;
function TgoObservable.GetFreeEvent: IgoFreeEvent;
begin
if (FOnFree = nil) then
FOnFree := TgoFreeEvent.Create;
Result := FOnFree;
end;
function TgoObservable.GetPropertyChangedEvent: IgoPropertyChangedEvent;
begin
if (FOnPropertyChanged = nil) then
FOnPropertyChanged := TgoPropertyChangedEvent.Create;
Result := FOnPropertyChanged;
end;
procedure TgoObservable.PropertyChanged(const APropertyName: String);
begin
if Assigned(FOnPropertyChanged) then
FOnPropertyChanged.Invoke(Self, APropertyName);
end;
{ TgoObservableCollection<T> }
function TgoObservableCollection<T>.Add(const AItem: T): Integer;
var
NPC: IgoNotifyPropertyChanged;
begin
Result := FList.Add(AItem);
if Supports(AItem, IgoNotifyPropertyChanged, NPC) then
NPC.GetPropertyChangedEvent.Add(DoItemPropertyChanged);
DoCollectionChanged(TgoCollectionChangedAction.Add, AItem, Result);
DoPropertyChanged('Count');
end;
procedure TgoObservableCollection<T>.AddRange(const AItems: array of T);
var
I, Index: Integer;
Item: T;
NPC: IgoNotifyPropertyChanged;
begin
Index := FList.Count;
FList.AddRange(AItems);
for I := 0 to Length(AItems) - 1 do
begin
Item := AItems[I];
if Supports(Item, IgoNotifyPropertyChanged, NPC) then
NPC.GetPropertyChangedEvent.Add(DoItemPropertyChanged);
DoCollectionChanged(TgoCollectionChangedAction.Add, Item, Index);
Inc(Index);
end;
if (Length(AItems) > 0) then
DoPropertyChanged('Count');
end;
procedure TgoObservableCollection<T>.AddRange(
const ACollection: TEnumerable<T>);
var
Index: Integer;
Item: T;
NPC: IgoNotifyPropertyChanged;
begin
Index := FList.Count;
FList.AddRange(ACollection);
for Item in ACollection do
begin
if Supports(Item, IgoNotifyPropertyChanged, NPC) then
NPC.GetPropertyChangedEvent.Add(DoItemPropertyChanged);
DoCollectionChanged(TgoCollectionChangedAction.Add, Item, Index);
Inc(Index);
end;
if Assigned(ACollection) then
DoPropertyChanged('Count');
end;
function TgoObservableCollection<T>.BinarySearch(const AItem: T;
out AIndex: Integer; const AComparer: IComparer<T>): Boolean;
begin
Result := FList.BinarySearch(AItem, AIndex, AComparer);
end;
function TgoObservableCollection<T>.BinarySearch(const AItem: T;
out AIndex: Integer): Boolean;
begin
Result := FList.BinarySearch(AItem, AIndex);
end;
procedure TgoObservableCollection<T>.Clear;
var
I: Integer;
Item: T;
NPC: IgoNotifyPropertyChanged;
begin
if (FList.Count > 0) then
begin
for I := 0 to FList.Count - 1 do
begin
Item := FList[I];
if Supports(Item, IgoNotifyPropertyChanged, NPC) then
begin
NPC.GetPropertyChangedEvent.Remove(DoItemPropertyChanged);
NPC := nil;
end;
end;
FList.Clear;
DoCollectionChanged(TgoCollectionChangedAction.Clear);
DoPropertyChanged('Count');
end;
end;
function TgoObservableCollection<T>.Contains(const AItem: T): Boolean;
begin
Result := FList.Contains(AItem)
end;
constructor TgoObservableCollection<T>.Create(const AOwnsObjects: Boolean);
begin
inherited Create;
FList := TObjectList<T>.Create;
FList.OwnsObjects := AOwnsObjects;
end;
procedure TgoObservableCollection<T>.Delete(const AIndex: Integer);
var
Item: T;
NPC: IgoNotifyPropertyChanged;
begin
Item := FList[AIndex];
if Supports(Item, IgoNotifyPropertyChanged, NPC) then
begin
NPC.GetPropertyChangedEvent.Remove(DoItemPropertyChanged);
{ Set NPC to nil BEFORE removing item from list. The list owns the item, so
deleting it frees the item, and NPC would be invalid and an Access
Violation would happen when NPC would be cleaned up as it goes out of
scope. }
NPC := nil;
end;
FList.Delete(AIndex);
DoCollectionChanged(TgoCollectionChangedAction.Delete, nil, AIndex);
DoPropertyChanged('Count');
end;
procedure TgoObservableCollection<T>.DeleteRange(const AIndex, ACount: Integer);
var
I: Integer;
Item: T;
NPC: IgoNotifyPropertyChanged;
begin
for I := AIndex to AIndex + ACount - 1 do
begin
Item := FList[I];
if Supports(Item, IgoNotifyPropertyChanged, NPC) then
begin
NPC.GetPropertyChangedEvent.Remove(DoItemPropertyChanged);
NPC := nil;
end;
end;
FList.DeleteRange(AIndex, ACount);
for I := AIndex to AIndex + ACount - 1 do
DoCollectionChanged(TgoCollectionChangedAction.Delete, nil, I);
if (ACount > 0) then
DoPropertyChanged('Count');
end;
destructor TgoObservableCollection<T>.Destroy;
begin
FList.Free;
inherited;
end;
procedure TgoObservableCollection<T>.DoCollectionChanged(
const AAction: TgoCollectionChangedAction; const AItem: TObject;
const AItemIndex: Integer; const APropertyName: String);
var
Args: TgoCollectionChangedEventArgs;
begin
if Assigned(FOnCollectionChanged) then
begin
Args := TgoCollectionChangedEventArgs.Create(AAction, AItem, AItemIndex, APropertyName);
FOnCollectionChanged.Invoke(Self, Args);
end;
end;
function TgoObservableCollection<T>.DoGetEnumerator: TEnumerator<T>;
begin
Result := FList.GetEnumerator;
end;
procedure TgoObservableCollection<T>.DoItemPropertyChanged(
const ASender: TObject; const APropertyName: String);
begin
DoCollectionChanged(TgoCollectionChangedAction.ItemChange, ASender, -1,
APropertyName);
end;
procedure TgoObservableCollection<T>.DoPropertyChanged(
const APropertyName: String);
begin
if Assigned(FOnPropertyChanged) then
FOnPropertyChanged.Invoke(Self, APropertyName);
end;
function TgoObservableCollection<T>.First: T;
begin
Result := FList.First;
end;
function TgoObservableCollection<T>.GetCapacity: Integer;
begin
Result := FList.Capacity;
end;
function TgoObservableCollection<T>.GetCollectionChangedEvent: IgoCollectionChangedEvent;
begin
if (FOnCollectionChanged = nil) then
FOnCollectionChanged := TgoCollectionChangedEvent.Create;
Result := FOnCollectionChanged;
end;
function TgoObservableCollection<T>.GetCount: Integer;
begin
Result := FList.Count;
end;
function TgoObservableCollection<T>.GetItem(const AIndex: Integer): T;
begin
Result := FList[AIndex];
end;
function TgoObservableCollection<T>.GetPropertyChangedEvent: IgoPropertyChangedEvent;
begin
if (FOnPropertyChanged = nil) then
FOnPropertyChanged := TgoPropertyChangedEvent.Create;
Result := FOnPropertyChanged;
end;
function TgoObservableCollection<T>.IndexOf(const AItem: T): Integer;
begin
Result := FList.IndexOf(AItem);
end;
function TgoObservableCollection<T>.IndexOfItem(const AItem: T;
const ADirection: TDirection): Integer;
begin
Result := FList.IndexOfItem(AItem, ADirection);
end;
procedure TgoObservableCollection<T>.Insert(const AIndex: Integer;
const AItem: T);
var
NPC: IgoNotifyPropertyChanged;
begin
FList.Insert(AIndex, AItem);
if Supports(AItem, IgoNotifyPropertyChanged, NPC) then
NPC.GetPropertyChangedEvent.Add(DoItemPropertyChanged);
DoCollectionChanged(TgoCollectionChangedAction.Add, AItem, AIndex);
DoPropertyChanged('Count');
end;
procedure TgoObservableCollection<T>.InsertRange(const AIndex: Integer;
const AItems: array of T);
var
I, Index: Integer;
Item: T;
NPC: IgoNotifyPropertyChanged;
begin
Index := AIndex;
FList.InsertRange(AIndex, AItems);
for I := 0 to Length(AItems) - 1 do
begin
Item := AItems[I];
if Supports(Item, IgoNotifyPropertyChanged, NPC) then
NPC.GetPropertyChangedEvent.Add(DoItemPropertyChanged);
DoCollectionChanged(TgoCollectionChangedAction.Add, Item, Index);
Inc(Index);
end;
if (Length(AItems) > 0) then
DoPropertyChanged('Count');
end;
procedure TgoObservableCollection<T>.InsertRange(const AIndex: Integer;
const ACollection: TEnumerable<T>);
var
Index: Integer;
Item: T;
NPC: IgoNotifyPropertyChanged;
begin
Index := AIndex;
FList.InsertRange(AIndex, ACollection);
for Item in ACollection do
begin
if Supports(Item, IgoNotifyPropertyChanged, NPC) then
NPC.GetPropertyChangedEvent.Add(DoItemPropertyChanged);
DoCollectionChanged(TgoCollectionChangedAction.Add, Item, Index);
Inc(Index);
end;
if Assigned(ACollection) then
DoPropertyChanged('Count');
end;
function TgoObservableCollection<T>.Last: T;
begin
Result := FList.Last;
end;
function TgoObservableCollection<T>.LastIndexOf(const AItem: T): Integer;
begin
Result := FList.LastIndexOf(AItem);
end;
function TgoObservableCollection<T>.QueryInterface(const IID: TGUID;
out Obj): HResult;
begin
if GetInterface(IID, Obj) then
Result := S_OK
else
Result := E_NOINTERFACE;
end;
function TgoObservableCollection<T>.Remove(const AItem: T): Integer;
var
NPC: IgoNotifyPropertyChanged;
begin
if Supports(AItem, IgoNotifyPropertyChanged, NPC) then
begin
NPC.GetPropertyChangedEvent.Remove(DoItemPropertyChanged);
NPC := nil;
end;
Result := FList.Remove(AItem);
if (Result >= 0) then
begin
DoCollectionChanged(TgoCollectionChangedAction.Delete, nil, Result);
DoPropertyChanged('Count');
end;
end;
procedure TgoObservableCollection<T>.Reverse;
begin
FList.Reverse;
DoCollectionChanged(TgoCollectionChangedAction.Rearrange);
end;
procedure TgoObservableCollection<T>.SetItem(const AIndex: Integer;
const Value: T);
begin
FList[AIndex] := Value;
end;
procedure TgoObservableCollection<T>.Sort;
begin
FList.Sort;
DoCollectionChanged(TgoCollectionChangedAction.Rearrange);
end;
procedure TgoObservableCollection<T>.Sort(const AComparer: IComparer<T>);
begin
FList.Sort(AComparer);
DoCollectionChanged(TgoCollectionChangedAction.Rearrange);
end;
function TgoObservableCollection<T>.ToArray: TArray<T>;
begin
Result := FList.ToArray;
end;
procedure TgoObservableCollection<T>.TrimExcess;
begin
FList.TrimExcess;
end;
function TgoObservableCollection<T>._AddRef: Integer;
begin
Result := -1;
end;
function TgoObservableCollection<T>._Release: Integer;
begin
Result := -1;
end;
end.