ferroui_dynamic_data/binding/abstract_notify_property_changed.rs
1//! `AbstractNotifyPropertyChanged` (`Binding/AbstractNotifyPropertyChanged.cs`): the base of the
2//! objects that raise `PropertyChanged`, as a part that a type embeds.
3
4use ferroui_reactive::disposables::Disposable;
5use ferroui_system::component_model::INotifyPropertyChanged;
6use ferroui_system::events::Event;
7use ferroui_system::IDisposable;
8use std::cell::Cell;
9use std::fmt;
10use std::rc::Rc;
11
12/// A backing field that [`AbstractNotifyPropertyChanged::set_and_raise`] reads and assigns: a
13/// `Cell` for a value that is `Copy`, a `RefCell` for the others (`ref T backingField`). It is
14/// `ferroui-system`'s, which the reactive objects of `ferroui-reactiveui` share.
15pub use ferroui_system::component_model::BackingField;
16
17struct Inner {
18 property_changed: Event<str>,
19 is_suspended: Cell<bool>,
20}
21
22/// The base of an object that notifies the changes of its properties
23/// (`AbstractNotifyPropertyChanged`).
24///
25/// Deviation (DEVIATIONS.md, DynamicData): upstream's abstract class is a part that a type embeds;
26/// the type implements `INotifyPropertyChanged` by returning [`property_changed`](Self::property_changed),
27/// and its setters call [`set_and_raise`](Self::set_and_raise) with the property name that
28/// `[CallerMemberName]` gives upstream (the PascalCase name of the getter, which `prop!` derives).
29///
30/// ```
31/// use ferroui_dynamic_data::binding::AbstractNotifyPropertyChanged;
32/// use ferroui_system::component_model::INotifyPropertyChanged;
33/// use ferroui_system::events::Event;
34/// use std::cell::Cell;
35///
36/// #[derive(Default)]
37/// struct Item { notify: AbstractNotifyPropertyChanged, age: Cell<i32> }
38/// impl Item {
39/// fn age(&self) -> i32 { self.age.get() }
40/// fn set_age(&self, value: i32) { self.notify.set_and_raise(&self.age, value, "Age") }
41/// }
42/// impl INotifyPropertyChanged for Item {
43/// fn property_changed(&self) -> &Event<str> { self.notify.property_changed() }
44/// }
45///
46/// let item = Item::default();
47/// item.set_age(3);
48/// assert_eq!(item.age(), 3);
49/// ```
50pub struct AbstractNotifyPropertyChanged {
51 inner: Rc<Inner>,
52}
53
54impl Default for AbstractNotifyPropertyChanged {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl fmt::Debug for AbstractNotifyPropertyChanged {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.debug_struct("AbstractNotifyPropertyChanged")
63 .field("handlers", &self.inner.property_changed.handler_count())
64 .field("is_suspended", &self.inner.is_suspended.get())
65 .finish()
66 }
67}
68
69impl AbstractNotifyPropertyChanged {
70 /// A part without handlers.
71 pub fn new() -> Self {
72 Self { inner: Rc::new(Inner { property_changed: Event::new(), is_suspended: Cell::new(false) }) }
73 }
74
75 /// The event raised when a property value has changed (`PropertyChanged`).
76 pub fn property_changed(&self) -> &Event<str> {
77 &self.inner.property_changed
78 }
79
80 /// Raises `PropertyChanged` for `property_name`; an empty name means every property
81 /// (`OnPropertyChanged(string? propertyName)`, where `null` is every property). Nothing is
82 /// raised while the notifications are suspended.
83 pub fn on_property_changed(&self, property_name: &str) {
84 Self::raise(&self.inner, property_name);
85 }
86
87 fn raise(inner: &Inner, property_name: &str) {
88 if inner.is_suspended.get() {
89 return;
90 }
91 inner.property_changed.raise(property_name);
92 }
93
94 /// Assigns `value` to `field` and raises `PropertyChanged` for `property_name`, unless the
95 /// field already holds an equal value (`SetAndRaise<T>(ref T, T, string?)`).
96 pub fn set_and_raise<T: PartialEq>(&self, field: &impl BackingField<T>, value: T, property_name: &str) {
97 self.set_and_raise_with(field, value, |current, value| current == value, property_name);
98 }
99
100 /// [`set_and_raise`](Self::set_and_raise) with an equality comparer
101 /// (`SetAndRaise<T>(ref T, T, IEqualityComparer<T>?, string?)`).
102 pub fn set_and_raise_with<T>(&self, field: &impl BackingField<T>, value: T, comparer: impl Fn(&T, &T) -> bool, property_name: &str) {
103 if field.equals(&value, &comparer) {
104 return;
105 }
106 field.store(value);
107 self.on_property_changed(property_name);
108 }
109
110 /// Suspends the notifications until the result is disposed; then, when
111 /// `invoke_property_changed_event_when_finished` is true, raises `PropertyChanged` for every
112 /// property (`SuspendNotifications(bool invokePropertyChangeEventWhenDisposed = true)`).
113 ///
114 /// Deviation (DEVIATIONS.md, DynamicData): the reference checkout no longer has this member;
115 /// the earlier API marked it obsolete ("This never worked properly in the first place"). It is
116 /// ported from that API: the suspension is one flag, so the first disposal of nested
117 /// suspensions ends it.
118 pub fn suspend_notifications(&self, invoke_property_changed_event_when_finished: bool) -> Rc<dyn IDisposable> {
119 self.inner.is_suspended.set(true);
120 let inner = self.inner.clone();
121 Disposable::create(move || {
122 inner.is_suspended.set(false);
123 if invoke_property_changed_event_when_finished {
124 Self::raise(&inner, "");
125 }
126 })
127 }
128}
129
130impl INotifyPropertyChanged for AbstractNotifyPropertyChanged {
131 fn property_changed(&self) -> &Event<str> {
132 &self.inner.property_changed
133 }
134}