Skip to content

Commit

Permalink
grrrr
Browse files Browse the repository at this point in the history
  • Loading branch information
sharpchen committed Oct 29, 2024
1 parent 0856954 commit 4b96188
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 16 deletions.
6 changes: 0 additions & 6 deletions docs/components/DocumentLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,5 @@
import Copyright from '../components/Copyright.vue';
import { ref } from 'vue';
import Layout from 'vitepress/dist/client/theme-default/Layout.vue';
import { useSidebar } from 'vitepress/theme';
const layout = ref<typeof Layout | null>(null);
const { toggle, isOpen, close, hasSidebar } = useSidebar()
if (hasSidebar.value)
if (isOpen.value) {
close()
}
</script>
207 changes: 207 additions & 0 deletions docs/document/Csharp Design Patterns/docs/Behavioural/Observer.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,210 @@ class PlayerObserver : IObserver<PlayerEventArgs>
}
}
```


## Observable Collection

`BindingList<T>` is a collection type with builtin event to tracked on collection manipulations.

```cs
using System.ComponentModel;

new Weather().Measure(); // [!code highlight]
class Weather
{
public BindingList<float> Tempretures { get; } = [];
public Weather()
{
// BindingList has a builtin event on manipulation
Tempretures.ListChanged += (sender, args) => // [!code highlight]
{ // [!code highlight]
if (args.ListChangedType == ListChangedType.ItemAdded) // [!code highlight]
{ // [!code highlight]
var newtempreture = (sender as BindingList<float>)?[args.NewIndex]; // [!code highlight]
Console.WriteLine($"New tempreture {newtempreture} degree has been added."); // [!code highlight]
} // [!code highlight]
}; // [!code highlight]
}
public void Measure()
{
Tempretures.Add(Random.Shared.NextSingle() * 100);
}
}
```

> [!WARNING]
> `BindingList<T>` can only track manipulations, can't track on status.
> For those purposes, you should add custom events.

## Property Observer

Use `INotifyPropertyChanged` for watching properties.
`PropertyChangedEventArgs` takes only `PropertyName`

```cs
using System.ComponentModel;

Player player = new() { Id = 1 };
Player? enemy = new() { Id = 2 };
player.Attack(enemy, 100); // [!code highlight]
class Player : INotifyPropertyChanged
{
public int Id { get; init; }
public int Health { get; private set; } = 100;

public event PropertyChangedEventHandler? PropertyChanged; // [!code highlight]
public Player()
{
PropertyChanged += (sender, args) =>
{
Console.WriteLine($"Property `{args.PropertyName}` of {(sender as Player)?.Id ?? -1} changed!");
};
}
public void Attack(Player enemy, int damage)
{
enemy.Health -= damage;
Console.WriteLine($"enemy {Id} been attacked by player {enemy.Id} with damage {damage}");
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Health)));
}
}
```

### Bidirectional Observer

A bidirectional observer means two objects subscribe each other, will get notification on no matter which side.
So, a common approach is implementing `INotifyPropertyChanged` and append event handlers for both.

```cs
using System.ComponentModel;
using System.Runtime.CompilerServices;

var init = "Hello"; // [!code highlight]
View view = new() { InnerText = init }; // [!code highlight]
TextBlock textBlock = new() { Text = init }; // [!code highlight]
// [!code highlight]
view.PropertyChanged += (sender, args) => // [!code highlight]
{ // [!code highlight]
if (args.PropertyName == nameof(View.InnerText)) // [!code highlight]
{ // [!code highlight]
Console.WriteLine($"Property {typeof(View).Name}.{nameof(View.InnerText)} has changed."); // [!code highlight]
textBlock.Text = view.InnerText; // also updates for another side // [!code highlight]
} // [!code highlight]
}; // [!code highlight]
// [!code highlight]
textBlock.PropertyChanged += (sender, args) => // [!code highlight]
{ // [!code highlight]
if (args.PropertyName == nameof(TextBlock.Text)) // [!code highlight]
{ // [!code highlight]
Console.WriteLine($"Property {typeof(TextBlock).Name}.{nameof(TextBlock.Text)} has changed."); // [!code highlight]
view.InnerText = textBlock.Text; // also updates for another side // [!code highlight]
} // [!code highlight]
}; // [!code highlight]
view.InnerText = "World"; // [!code highlight]
// Property View.InnerText has changed. // [!code highlight]
// Property TextBlock.Text has changed. // [!code highlight]
Console.WriteLine(view.InnerText); // <- World // [!code highlight]
Console.WriteLine(textBlock.Text); // <- World // [!code highlight]
class TextBlock : INotifyPropertyChanged
{
private string? text;

public string? Text
{
get => text;
set
{
if (value == text) return; // [!code highlight]
text = value;
OnPropertyChanged(); // [!code highlight]
}
}

public event PropertyChangedEventHandler? PropertyChanged;

protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class View : INotifyPropertyChanged
{
private string? innerText;

public string? InnerText
{
get => innerText;
set
{
if (value == innerText) return; // [!code highlight]
innerText = value;
OnPropertyChanged(); // [!code highlight]
}
}

public event PropertyChangedEventHandler? PropertyChanged;

protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
```

> [!NOTE]
> An interesting part is, bidirectional observer above does not cause stack overflow.
> simply because a guardian `if(value == prop) return` is inside setter.
### Bidirectional Binding

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.
So, a custom generic class for performing the mechanism if required.

```cs
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

var init = "Hello";
View view = new() { InnerText = init };
TextBlock textBlock = new() { Text = init };

var _ = new BidirectionalBinding<View, TextBlock>(
view,
v => v.InnerText, // selects which property to track // [!code highlight]
textBlock,
t => t.Text // [!code highlight]
);

view.InnerText = "World"; // [!code highlight]
Console.WriteLine(view.InnerText); // <- World // [!code highlight]
Console.WriteLine(textBlock.Text); // <- World // [!code highlight]
class BidirectionalBinding<TFirst, TSecond>
where TFirst : INotifyPropertyChanged // both should be `INotifyPropertyChanged` // [!code highlight]
where TSecond : INotifyPropertyChanged
{
public BidirectionalBinding(
TFirst first,
Expression<Func<TFirst, object?>> firstSelector,
TSecond second,
Expression<Func<TSecond, object?>> secondSelector)
{
if (firstSelector.Body is MemberExpression firExpr && secondSelector.Body is MemberExpression secExpr)
{
if (firExpr.Member is PropertyInfo firProp && secExpr.Member is PropertyInfo secProp)
{
first.PropertyChanged += (sender, args) => secProp.SetValue(second, firProp.GetValue(first)); // [!code highlight]
second.PropertyChanged += (sender, args) => firProp.SetValue(first, secProp.GetValue(second)); // [!code highlight]
}
}
}
}
```
20 changes: 10 additions & 10 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ layout: home
title: "Home"
markdownStyles: false
hero:
name: "Articles"
image:
# src: /favicon.ico
alt: sharpchen
name: "Articles"
image:
# src: /favicon.ico
alt: sharpchen
---

<VPFeatures :features="articleFeature"/>
<VPHero name="Documents"/>
<VPFeatures :features="features"/>
<VPFeatures :features="articleFeature" />
<VPHero name="Documents" />
<VPFeatures :features="features" />

<script lang="ts" setup>
import Enumerable from 'linq';
import Enumerable from 'linq';
import VPFeatures, { type Feature } from 'vitepress/dist/client/theme-default/components/VPFeatures.vue';
import VPHero from 'vitepress/dist/client/theme-default/components/VPHero.vue';
import { ref } from 'vue';
import { data } from './data/Features.data';
const features: Feature[] = data.features;
const articleFeature = ref(data.articleFeature);
const features: Feature[] = data.features;
const articleFeature = ref(data.articleFeature);
</script>
12 changes: 12 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"include": [
"docs/**/*.ts",
"docs/**/*.vue",
"docs/**/*.md"
],
"vueCompilerOptions": {
"vitePressExtensions": [
".md"
]
}
}

0 comments on commit 4b96188

Please sign in to comment.