Skip to content

Commit 0b420b0

Browse files
committed
grrr
1 parent ace67a6 commit 0b420b0

File tree

9 files changed

+258
-17
lines changed

9 files changed

+258
-17
lines changed

docs/.vitepress/theme/custom.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
:root {
22
--vp-home-hero-name-color: #fa5c15;
33
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, #fa5c15, rgb(251, 70, 202));
4-
--vp-font-family-mono: 'JetBrains Mono NL', 'Noto Sans Mono', 'SF Mono SC', 'SF Mono', 'Cascadia Code', Menlo, 'JetBrains Mono', Monaco,
4+
--vp-font-family-mono: 'JetBrains Mono NL', 'JetBrains Mono', 'Fira Code', 'Noto Sans Mono', 'SF Mono SC', 'SF Mono', 'Cascadia Code', Menlo, Monaco,
55
Consolas, 'Liberation Mono', 'Courier New', 'Nerd Font Symbols', ui-monospace, monospace;
66
--vp-font-family-base: 'Chinese Quotes', 'Inter var', 'Inter', ui-sans-serif,
77
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,

docs/document/Avalonia/docs/1. Fundamentals/3. MVVM Essentials.md

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Introduction
2+
3+
```mermaid
4+
graph TD;
5+
A[View] -->|binds to| B[ViewModel]
6+
B -->|notifies| A
7+
B -->|interacts with| C[Model]
8+
C -->|updates| B
9+
A -->|displays data from| C
10+
```
11+
12+
This chapter introduces the common usage of MVVM pattern with `CommunityToolkit.Mvvm`.
13+
14+
## Model
15+
16+
Model is basically a data shape, it describes the entity.
17+
A model should pure with a bunch of properties only.(May include implicit conversion operators.)
18+
19+
## ViewModel
20+
21+
ViewModel is the mediator for Model and View.
22+
Isolates the two, making View completely ignorant to Model.(Model surely don't know about View, it's just a pure shape.)
23+
It represents the status of a Model instance as a Observable with extra logic for creating or manipulating itself.
24+
25+
## View
26+
27+
- defining properties to store necessary data for displaying on UI.
28+
- defining commands to be triggered when interacting with users.
29+
- every data type represented as observable should be a ViewModel.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Defining a ViewModel
2+
3+
A ViewModel should be a `ObservableObject`
4+
5+
## How Avalonia Do by Default
6+
7+
Default template of `avalonia.mvvm` generates a class `ViewModelBase` as base observable type to share across the project.
8+
9+
```cs
10+
using CommunityToolkit.Mvvm.ComponentModel;
11+
12+
namespace ToDoList.ViewModels;
13+
14+
public class ViewModelBase : ObservableObject // nah I don't like it. // [!code highlight]
15+
{
16+
}
17+
```
18+
19+
But this is rather confusing for beginners. so manually inherit from `ObservableObject` is a good start.
20+
21+
```cs
22+
using CommunityToolkit.Mvvm.ComponentModel;
23+
24+
namespace ToDoList.ViewModels;
25+
26+
public partial class MainWindowViewModel : ObservableObject // [!code highlight]
27+
{
28+
}
29+
```
30+
31+
## What's behind `ObservableObject`
32+
33+
`ObservableObject` is both `INotifyPropertyChanged` and `INotifyPropertyChanging`, implementing the two events.
34+
35+
```cs
36+
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging { /* ... */ }
37+
```
38+
39+
There's also a `SetProperty` method for **performing the observer pattern.**
40+
Notice a `ref` modifier appears here, it's just for making sure we can bind a value type instance.
41+
42+
```cs
43+
protected bool SetProperty<T>([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, [CallerMemberName] string? propertyName = null)
44+
{
45+
// We duplicate the code here instead of calling the overload because we can't
46+
// guarantee that the invoked SetProperty<T> will be inlined, and we need the JIT
47+
// to be able to see the full EqualityComparer<T>.Default.Equals call, so that
48+
// it'll use the intrinsics version of it and just replace the whole invocation
49+
// with a direct comparison when possible (eg. for primitive numeric types).
50+
// This is the fastest SetProperty<T> overload so we particularly care about
51+
// the codegen quality here, and the code is small and simple enough so that
52+
// duplicating it still doesn't make the whole class harder to maintain.
53+
if (EqualityComparer<T>.Default.Equals(field, newValue)) // [!code highlight]
54+
{ // [!code highlight]
55+
return false; // [!code highlight]
56+
} // [!code highlight]
57+
58+
OnPropertyChanging(propertyName);
59+
60+
field = newValue;
61+
62+
OnPropertyChanged(propertyName);
63+
64+
return true;
65+
}
66+
```
67+
68+
> [!NOTE]
69+
> There's multiple overloads of `SetProperty`, it's just one of the most commonly used.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# ObservableProperty
2+
3+
Another useful attribute from `CommunityToolkit.Mvvm`, by using `ObservableProperty`, we can get rid of `ObservableObject.SetProperty` we mentioned earlier.
4+
5+
## How is `ObservableObject.SetProperty` been used
6+
7+
`SetProperty` saves us from the boilerplate for each observable property.
8+
9+
```cs
10+
class Foo : ObservableObject
11+
{
12+
private bool _isChecked;
13+
14+
public bool IsChecked
15+
{
16+
get => _isChecked;
17+
set => SetProperty(ref _isChecked, value); // [!code highlight]
18+
}
19+
}
20+
```
21+
22+
`ObservableProperty` saves even more. It's empowered by source generator, which needs an extra `partial` mark for emiting code at somewhere else.
23+
24+
```cs
25+
partial class Foo : ObservableObject // requires partial to emit code // [!code highlight]
26+
{
27+
[ObservableProperty] private bool _isChecked; // [!code highlight]
28+
}
29+
```
30+
31+
The code generated is like the following, performs the similar logic as `SetProperty`.
32+
33+
```cs
34+
partial class Foo
35+
{
36+
/// <inheritdoc cref="_isChecked"/>
37+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.3.0.0")]
38+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
39+
public bool IsChecked
40+
{
41+
get => _isChecked;
42+
set
43+
{
44+
if (!global::System.Collections.Generic.EqualityComparer<bool>.Default.Equals(_isChecked, value)) // [!code highlight]
45+
{
46+
// also generates a lot methods for the field only.
47+
OnIsCheckedChanging(value);
48+
OnIsCheckedChanging(default, value);
49+
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.IsChecked);
50+
_isChecked = value;
51+
OnIsCheckedChanged(value);
52+
OnIsCheckedChanged(default, value);
53+
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.IsChecked);
54+
}
55+
}
56+
}
57+
58+
// ...
59+
}
60+
```

docs/document/Avalonia/docs/MVVM Essentials/4. Trigger a Command.md

Whitespace-only changes.

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

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ class Weather
285285
## Property Observer
286286

287287
Use `INotifyPropertyChanged` for watching properties.
288-
`PropertyChangedEventArgs` takes only `PropertyName`
288+
**`INotifyPropertyChanged` implies the object implementing this interface should have a ability to be informed when certain property changed.**
289+
The event handler generated takes `PropertyChangedEventArgs` which has only a property named as `PropertyName`.
289290

290291
```cs
291292
using System.ComponentModel;
@@ -451,3 +452,98 @@ class BidirectionalBinding<TFirst, TSecond>
451452
}
452453
}
453454
```
455+
456+
> [!NOTE]
457+
> `BidirectionalBinding` should actually be a `IDisposable`, but I failed to find a way to deleted the event handlers.
458+
459+
## Property Dependencies (WIP)
460+
461+
On problem of property observer is, multiple property tracking is possible unless they have dependencies among.
462+
463+
Given a example of a property for conditional status `IsDying` which is dependent on property `Health`.
464+
465+
Once `Health` changed, `IsDying` might changed too, but would need a extra check to make sure it doesn't notify on each change of `Health`.
466+
**But this will explode when you have many many dependent properties.**
467+
468+
```cs
469+
using System.ComponentModel;
470+
using System.Runtime.CompilerServices;
471+
472+
Player p = new();
473+
p.Health -= 99; // [!code highlight]
474+
// Property Health changed
475+
// Property IsDying changed
476+
477+
class Player : INotifyPropertyChanged
478+
{
479+
private int health = 100;
480+
481+
public int Health
482+
{
483+
get => health;
484+
set
485+
{
486+
if (value == health) return;
487+
var isDying = IsDying; // [!code highlight]
488+
health = value;
489+
OnPropertyChanged();
490+
if (isDying != IsDying) // [!code highlight]
491+
OnPropertyChanged(nameof(IsDying)); // [!code highlight]
492+
}
493+
}
494+
495+
public bool IsDying => Health < 5; // [!code highlight]
496+
497+
public Player()
498+
{
499+
PropertyChanged += (sender, args) =>
500+
Console.WriteLine($"Property {args.PropertyName} changed.");
501+
}
502+
503+
public event PropertyChangedEventHandler? PropertyChanged;
504+
505+
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
506+
{
507+
PropertyChanged?.Invoke(this, new(propertyName));
508+
}
509+
}
510+
```
511+
512+
We'll have to use a base class to handle the default behaviors.
513+
Each property might have multiple dependencies and each dependency should be unique since they're inside the same class.
514+
So a map as `Dictionary<string, HashSet<string>>` appears here and the `OnPropertyChanged` should walk each property recursively.
515+
516+
```cs
517+
abstract class PropertyNotificationSupport : INotifyPropertyChanged
518+
{
519+
private readonly Dictionary<string, HashSet<string>> _depedendcyMap = []; // stores property name and its dependent properties. // [!code highlight]
520+
public event PropertyChangedEventHandler? PropertyChanged;
521+
522+
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
523+
{
524+
PropertyChanged?.Invoke(this, new(propertyName));
525+
foreach (var prop in _depedendcyMap.Keys)
526+
{
527+
// for props relied on the coming property
528+
// notify them recursively
529+
if (_depedendcyMap[propertyName].Contains(prop))
530+
OnPropertyChanged(prop); // recursive call on other property dependencies // [!code highlight]
531+
}
532+
}
533+
}
534+
```
535+
536+
Yet another target is not only tracking the single property name but also sending with which property change causes the change of another property.
537+
Which means the literal property dependency graph.
538+
539+
The solution is using `Expression`, to assign both evaluation logic and name of the property with one shot. We should have a method inside base class to do that.
540+
541+
```cs
542+
private readonly Func<bool> _isDying; // [!code highlight]
543+
public bool IsDying => _isDying();
544+
public Player()
545+
{
546+
// implement `Property` method in the base class.
547+
IsDying = Property(nameof(IsDying), () => Health < 5); // [!code highlight]
548+
}
549+
```

docs/index.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ alt: sharpchen
1515
<VPFeatures :features="features" />
1616

1717
<script lang="ts" setup>
18-
import Enumerable from 'linq';
1918
import VPFeatures, { type Feature } from 'vitepress/dist/client/theme-default/components/VPFeatures.vue';
2019
import VPHero from 'vitepress/dist/client/theme-default/components/VPHero.vue';
2120
import { ref } from 'vue';

docs/services/DocumentService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type DocumentInfo = Record<string, { icon: string; description: string }>
88

99
export const documentMap = {
1010
'Csharp Design Patterns': { icon: '👾', description: 'Design Patterns in C#' },
11-
'Modern CSharp': { icon: '🦖', description: 'Modernized C# since 2015?' },
11+
// 'Modern CSharp': { icon: '🦖', description: 'Modernized C# since 2015?' },
1212
Articles: { icon: '📰', description: 'Regular articles' },
1313
Avalonia: { icon: '😱', description: 'AvaloniaUI' },
1414
Docker: { icon: '🐳', description: 'Ultimate Docker' },
@@ -17,7 +17,7 @@ export const documentMap = {
1717
SQL: { icon: '🦭', description: '' },
1818
TypeScript: { icon: '🤯', description: '' },
1919
// VBA: { icon: '💩', description: 'VBA for excel' },
20-
Vue3: { icon: '⚡', description: '' },
20+
// Vue3: { icon: '⚡', description: '' },
2121
'Unsafe CSharp': { icon: '😎', description: 'Entering the danger zone...' },
2222
'NeoVim ColorScheme Development': {
2323
icon: '🎨',

0 commit comments

Comments
 (0)