Skip to content

Commit 4b96188

Browse files
committed
grrrr
1 parent 0856954 commit 4b96188

File tree

4 files changed

+229
-16
lines changed

4 files changed

+229
-16
lines changed

docs/components/DocumentLayout.vue

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,5 @@
88
import Copyright from '../components/Copyright.vue';
99
import { ref } from 'vue';
1010
import Layout from 'vitepress/dist/client/theme-default/Layout.vue';
11-
import { useSidebar } from 'vitepress/theme';
1211
const layout = ref<typeof Layout | null>(null);
13-
const { toggle, isOpen, close, hasSidebar } = useSidebar()
14-
if (hasSidebar.value)
15-
if (isOpen.value) {
16-
close()
17-
}
1812
</script>

docs/document/Csharp Design Patterns/docs/Behavioural/Observer.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
```

docs/index.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@ layout: home
44
title: "Home"
55
markdownStyles: false
66
hero:
7-
name: "Articles"
8-
image:
9-
# src: /favicon.ico
10-
alt: sharpchen
7+
name: "Articles"
8+
image:
9+
# src: /favicon.ico
10+
alt: sharpchen
1111
---
1212

13-
<VPFeatures :features="articleFeature"/>
14-
<VPHero name="Documents"/>
15-
<VPFeatures :features="features"/>
13+
<VPFeatures :features="articleFeature" />
14+
<VPHero name="Documents" />
15+
<VPFeatures :features="features" />
1616

1717
<script lang="ts" setup>
18-
import Enumerable from 'linq';
18+
import Enumerable from 'linq';
1919
import VPFeatures, { type Feature } from 'vitepress/dist/client/theme-default/components/VPFeatures.vue';
2020
import VPHero from 'vitepress/dist/client/theme-default/components/VPHero.vue';
2121
import { ref } from 'vue';
2222
import { data } from './data/Features.data';
23-
const features: Feature[] = data.features;
24-
const articleFeature = ref(data.articleFeature);
23+
const features: Feature[] = data.features;
24+
const articleFeature = ref(data.articleFeature);
2525
</script>

tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"include": [
3+
"docs/**/*.ts",
4+
"docs/**/*.vue",
5+
"docs/**/*.md"
6+
],
7+
"vueCompilerOptions": {
8+
"vitePressExtensions": [
9+
".md"
10+
]
11+
}
12+
}

0 commit comments

Comments
 (0)