Skip to main content

ferroui_xaml_interactivity/helpers/
command_can_execute_observer.rs

1//! Port of `Helpers/CommandCanExecuteObserver.cs`.
2
3use ferroui_base::input::ICommand;
4use ferroui_base::reactive::IDisposable;
5use ferroui_base::BoxedValue;
6use std::cell::{Cell, RefCell};
7use std::rc::{Rc, Weak};
8
9/// Observes an [`ICommand`] and reports whether it can execute with a specified parameter.
10///
11/// Use this helper from custom command-backed behaviors to expose a bindable command state, such
12/// as a direct property. Call [`start`](Self::start) when the behavior is attached,
13/// [`update`](Self::update) when the command or command parameter changes, and
14/// [`stop`](Self::stop) when the behavior is detached.
15///
16/// The command event subscription uses a weak reference to this observer so a long-lived command
17/// does not keep the observer or owning behavior alive if cleanup is missed.
18///
19/// Deviation (DEVIATIONS.md, Behaviors): the reference posts the callback to the user-interface
20/// thread when a command raises its notification on another thread. Commands and observers of
21/// the port belong to the thread that created them (they are not `Send`), so the callback always
22/// runs on that thread.
23pub struct CommandCanExecuteObserver {
24    pub(crate) inner: Rc<ObserverState>,
25}
26
27pub(crate) struct ObserverState {
28    set_can_execute: Box<dyn Fn(bool)>,
29    weak_can_execute_changed_handler: Rc<WeakCanExecuteChangedHandler>,
30    command: RefCell<Option<Rc<dyn ICommand>>>,
31    parameter: RefCell<Option<BoxedValue>>,
32    is_parameter_known: Cell<bool>,
33    is_observing: Cell<bool>,
34}
35
36impl CommandCanExecuteObserver {
37    /// Initializes a new instance of the [`CommandCanExecuteObserver`] class; `set_can_execute`
38    /// receives the current command execution state.
39    pub fn new(set_can_execute: impl Fn(bool) + 'static) -> Self {
40        let inner = Rc::new_cyclic(|this: &Weak<ObserverState>| ObserverState {
41            set_can_execute: Box::new(set_can_execute),
42            weak_can_execute_changed_handler: Rc::new(WeakCanExecuteChangedHandler::new(this.clone())),
43            command: RefCell::new(None),
44            parameter: RefCell::new(None),
45            is_parameter_known: Cell::new(true),
46            is_observing: Cell::new(false),
47        });
48        Self { inner }
49    }
50
51    /// Starts observing the specified command and reports its initial execution state; a null
52    /// command reports that execution is allowed.
53    pub fn start(&self, command: Option<Rc<dyn ICommand>>, parameter: Option<BoxedValue>) {
54        self.start_with(command, parameter, true);
55    }
56
57    /// Starts observing the specified command and reports its initial execution state.
58    ///
59    /// `is_parameter_known` is true when `parameter` is the actual parameter to evaluate, false
60    /// when the parameter will only be available at execution time: the observer then reports
61    /// true without calling `can_execute`, which avoids probing commands with placeholder values
62    /// such as null when event arguments or picker results will provide the real parameter later.
63    pub fn start_with(&self, command: Option<Rc<dyn ICommand>>, parameter: Option<BoxedValue>, is_parameter_known: bool) {
64        let state = &self.inner;
65        if state.is_observing.get() {
66            self.update_with(command, parameter, is_parameter_known);
67            return;
68        }
69
70        state.is_observing.set(true);
71        *state.command.borrow_mut() = command.clone();
72        *state.parameter.borrow_mut() = parameter;
73        state.is_parameter_known.set(is_parameter_known);
74
75        if let Some(command) = command {
76            state.weak_can_execute_changed_handler.subscribe(&command);
77        }
78
79        state.update_can_execute();
80    }
81
82    /// Updates the observed command and command parameter. Has no effect until
83    /// [`start`](Self::start) has been called.
84    pub fn update(&self, command: Option<Rc<dyn ICommand>>, parameter: Option<BoxedValue>) {
85        self.update_with(command, parameter, true);
86    }
87
88    /// Updates the observed command and command parameter; see [`start_with`](Self::start_with)
89    /// for `is_parameter_known`. Has no effect until [`start`](Self::start) has been called.
90    pub fn update_with(&self, command: Option<Rc<dyn ICommand>>, parameter: Option<BoxedValue>, is_parameter_known: bool) {
91        let state = &self.inner;
92        if !state.is_observing.get() {
93            return;
94        }
95
96        let same_command = match (&*state.command.borrow(), &command) {
97            (Some(current), Some(command)) => Rc::ptr_eq(current, command),
98            (None, None) => true,
99            _ => false,
100        };
101        if !same_command {
102            state.weak_can_execute_changed_handler.unsubscribe();
103
104            *state.command.borrow_mut() = command.clone();
105
106            if let Some(command) = command {
107                state.weak_can_execute_changed_handler.subscribe(&command);
108            }
109        }
110
111        *state.parameter.borrow_mut() = parameter;
112        state.is_parameter_known.set(is_parameter_known);
113        state.update_can_execute();
114    }
115
116    /// Stops observing the current command and clears the stored command parameter.
117    pub fn stop(&self) {
118        let state = &self.inner;
119        if !state.is_observing.get() {
120            return;
121        }
122
123        state.weak_can_execute_changed_handler.unsubscribe();
124
125        *state.command.borrow_mut() = None;
126        *state.parameter.borrow_mut() = None;
127        state.is_parameter_known.set(true);
128        state.is_observing.set(false);
129    }
130}
131
132impl IDisposable for CommandCanExecuteObserver {
133    /// Stops observing the current command.
134    fn dispose(&self) {
135        self.stop();
136    }
137}
138
139impl ObserverState {
140    fn update_can_execute(&self) {
141        // The reference posts to the user-interface thread when called from another thread; see
142        // the deviation on the type.
143        self.update_can_execute_core();
144    }
145
146    fn update_can_execute_core(&self) {
147        if !self.is_observing.get() {
148            return;
149        }
150
151        let command = self.command.borrow().clone();
152        let command = match command {
153            Some(command) if self.is_parameter_known.get() => command,
154            _ => {
155                (self.set_can_execute)(true);
156                return;
157            }
158        };
159
160        let parameter = self.parameter.borrow().clone();
161        (self.set_can_execute)(command.can_execute(parameter.as_ref()));
162    }
163}
164
165/// The handler of the notification of a command: it refers to its observer weakly, and removes
166/// itself from the command when the observer is gone.
167struct WeakCanExecuteChangedHandler {
168    observer: Weak<ObserverState>,
169    subscription: RefCell<Option<Rc<dyn IDisposable>>>,
170}
171
172impl WeakCanExecuteChangedHandler {
173    fn new(observer: Weak<ObserverState>) -> Self {
174        Self { observer, subscription: RefCell::new(None) }
175    }
176
177    /// `command.CanExecuteChanged += handler` (and `SetCommand(command)`).
178    fn subscribe(self: &Rc<Self>, command: &Rc<dyn ICommand>) {
179        // The command holds the handler (the target of its delegate in the reference), and the
180        // handler holds the observer weakly.
181        let handler = self.clone();
182        let subscription = command.can_execute_changed(Rc::new(move || handler.on_can_execute_changed()));
183        let previous = self.subscription.borrow_mut().replace(subscription);
184        if let Some(previous) = previous {
185            previous.dispose();
186        }
187    }
188
189    /// `command.CanExecuteChanged -= handler` (and `SetCommand(null)`).
190    fn unsubscribe(&self) {
191        let subscription = self.subscription.borrow_mut().take();
192        if let Some(subscription) = subscription {
193            subscription.dispose();
194        }
195    }
196
197    fn on_can_execute_changed(&self) {
198        if let Some(observer) = self.observer.upgrade() {
199            observer.update_can_execute();
200            return;
201        }
202
203        self.unsubscribe();
204    }
205}