@@ -244,3 +244,210 @@ class PlayerObserver : IObserver<PlayerEventArgs>
244244 }
245245}
246246```
247+
248+
249+ ## Observable Collection
250+
251+ ` BindingList<T> ` is a collection type with builtin event to tracked on collection manipulations.
252+
253+ ``` cs
254+ using System .ComponentModel ;
255+
256+ new Weather ().Measure (); // [!code highlight]
257+
258+ class Weather
259+ {
260+ public BindingList <float > Tempretures { get ; } = [];
261+ public Weather ()
262+ {
263+ // BindingList has a builtin event on manipulation
264+ Tempretures .ListChanged += (sender , args ) => // [!code highlight]
265+ { // [!code highlight]
266+ if (args .ListChangedType == ListChangedType .ItemAdded ) // [!code highlight]
267+ { // [!code highlight]
268+ var newtempreture = (sender as BindingList <float >)? [args .NewIndex ]; // [!code highlight]
269+ Console .WriteLine ($" New tempreture {newtempreture } degree has been added." ); // [!code highlight]
270+ } // [!code highlight]
271+ }; // [!code highlight]
272+ }
273+ public void Measure ()
274+ {
275+ Tempretures .Add (Random .Shared .NextSingle () * 100 );
276+ }
277+ }
278+ ```
279+
280+ > [ !WARNING]
281+ > ` BindingList<T> ` can only track manipulations, can't track on status.
282+ > For those purposes, you should add custom events.
283+
284+
285+ ## Property Observer
286+
287+ Use ` INotifyPropertyChanged ` for watching properties.
288+ ` PropertyChangedEventArgs ` takes only ` PropertyName `
289+
290+ ``` cs
291+ using System .ComponentModel ;
292+
293+ Player player = new () { Id = 1 };
294+ Player ? enemy = new () { Id = 2 };
295+ player .Attack (enemy , 100 ); // [!code highlight]
296+
297+ class Player : INotifyPropertyChanged
298+ {
299+ public int Id { get ; init ; }
300+ public int Health { get ; private set ; } = 100 ;
301+
302+ public event PropertyChangedEventHandler ? PropertyChanged ; // [!code highlight]
303+
304+ public Player ()
305+ {
306+ PropertyChanged += (sender , args ) =>
307+ {
308+ Console .WriteLine ($" Property `{args .PropertyName }` of {(sender as Player )? .Id ?? - 1 } changed!" );
309+ };
310+ }
311+ public void Attack (Player enemy , int damage )
312+ {
313+ enemy .Health -= damage ;
314+ Console .WriteLine ($" enemy {Id } been attacked by player {enemy .Id } with damage {damage }" );
315+ PropertyChanged ? .Invoke (this , new PropertyChangedEventArgs (nameof (Health )));
316+ }
317+ }
318+ ```
319+
320+ ### Bidirectional Observer
321+
322+ A bidirectional observer means two objects subscribe each other, will get notification on no matter which side.
323+ So, a common approach is implementing ` INotifyPropertyChanged ` and append event handlers for both.
324+
325+ ``` cs
326+ using System .ComponentModel ;
327+ using System .Runtime .CompilerServices ;
328+
329+ var init = " Hello" ; // [!code highlight]
330+ View view = new () { InnerText = init }; // [!code highlight]
331+ TextBlock textBlock = new () { Text = init }; // [!code highlight]
332+ // [!code highlight]
333+ view .PropertyChanged += (sender , args ) => // [!code highlight]
334+ { // [!code highlight]
335+ if (args .PropertyName == nameof (View .InnerText )) // [!code highlight]
336+ { // [!code highlight]
337+ Console .WriteLine ($" Property {typeof (View ).Name }.{nameof (View .InnerText )} has changed." ); // [!code highlight]
338+ textBlock .Text = view .InnerText ; // also updates for another side // [!code highlight]
339+ } // [!code highlight]
340+ }; // [!code highlight]
341+ // [!code highlight]
342+ textBlock .PropertyChanged += (sender , args ) => // [!code highlight]
343+ { // [!code highlight]
344+ if (args .PropertyName == nameof (TextBlock .Text )) // [!code highlight]
345+ { // [!code highlight]
346+ Console .WriteLine ($" Property {typeof (TextBlock ).Name }.{nameof (TextBlock .Text )} has changed." ); // [!code highlight]
347+ view .InnerText = textBlock .Text ; // also updates for another side // [!code highlight]
348+ } // [!code highlight]
349+ }; // [!code highlight]
350+
351+ view .InnerText = " World" ; // [!code highlight]
352+ // Property View.InnerText has changed. // [!code highlight]
353+ // Property TextBlock.Text has changed. // [!code highlight]
354+ Console .WriteLine (view .InnerText ); // <- World // [!code highlight]
355+ Console .WriteLine (textBlock .Text ); // <- World // [!code highlight]
356+
357+ class TextBlock : INotifyPropertyChanged
358+ {
359+ private string ? text ;
360+
361+ public string ? Text
362+ {
363+ get => text ;
364+ set
365+ {
366+ if (value == text ) return ; // [!code highlight]
367+ text = value ;
368+ OnPropertyChanged (); // [!code highlight]
369+ }
370+ }
371+
372+ public event PropertyChangedEventHandler ? PropertyChanged ;
373+
374+ protected virtual void OnPropertyChanged ([CallerMemberName ] string ? propertyName = null )
375+ {
376+ PropertyChanged ? .Invoke (this , new PropertyChangedEventArgs (propertyName ));
377+ }
378+ }
379+ class View : INotifyPropertyChanged
380+ {
381+ private string ? innerText ;
382+
383+ public string ? InnerText
384+ {
385+ get => innerText ;
386+ set
387+ {
388+ if (value == innerText ) return ; // [!code highlight]
389+ innerText = value ;
390+ OnPropertyChanged (); // [!code highlight]
391+ }
392+ }
393+
394+ public event PropertyChangedEventHandler ? PropertyChanged ;
395+
396+ protected virtual void OnPropertyChanged ([CallerMemberName ] string ? propertyName = null )
397+ {
398+ PropertyChanged ? .Invoke (this , new PropertyChangedEventArgs (propertyName ));
399+ }
400+ }
401+ ```
402+
403+ > [ !NOTE]
404+ > An interesting part is, bidirectional observer above does not cause stack overflow.
405+ > simply because a guardian ` if(value == prop) return ` is inside setter.
406+
407+ ### Bidirectional Binding
408+
409+ Previous example shows a very tedious implementation for bidirectional observer, we don't really want to hard code everything for each pair of object we have.
410+ So, a custom generic class for performing the mechanism if required.
411+
412+ ``` cs
413+ using System .ComponentModel ;
414+ using System .Linq .Expressions ;
415+ using System .Reflection ;
416+ using System .Runtime .CompilerServices ;
417+
418+ var init = " Hello" ;
419+ View view = new () { InnerText = init };
420+ TextBlock textBlock = new () { Text = init };
421+
422+ var _ = new BidirectionalBinding <View , TextBlock >(
423+ view ,
424+ v => v .InnerText , // selects which property to track // [!code highlight]
425+ textBlock ,
426+ t => t .Text // [!code highlight]
427+ );
428+
429+ view .InnerText = " World" ; // [!code highlight]
430+ Console .WriteLine (view .InnerText ); // <- World // [!code highlight]
431+ Console .WriteLine (textBlock .Text ); // <- World // [!code highlight]
432+
433+ class BidirectionalBinding <TFirst , TSecond >
434+ where TFirst : INotifyPropertyChanged // both should be `INotifyPropertyChanged` // [!code highlight]
435+ where TSecond : INotifyPropertyChanged
436+ {
437+ public BidirectionalBinding (
438+ TFirst first ,
439+ Expression <Func <TFirst , object ?>> firstSelector ,
440+ TSecond second ,
441+ Expression <Func <TSecond , object ?>> secondSelector )
442+ {
443+ if (firstSelector .Body is MemberExpression firExpr && secondSelector .Body is MemberExpression secExpr )
444+ {
445+ if (firExpr .Member is PropertyInfo firProp && secExpr .Member is PropertyInfo secProp )
446+ {
447+ first .PropertyChanged += (sender , args ) => secProp .SetValue (second , firProp .GetValue (first )); // [!code highlight]
448+ second .PropertyChanged += (sender , args ) => firProp .SetValue (first , secProp .GetValue (second )); // [!code highlight]
449+ }
450+ }
451+ }
452+ }
453+ ```
0 commit comments