Skip to main content

ferroui_xaml_interactivity/actions/
invoke_command_action_base.rs

1//! Port of `Actions/InvokeCommandActionBase.cs`.
2
3use crate::helpers::{CommandCanExecuteIsEnabledBinder, CommandCanExecuteObserver};
4use crate::{IActionLogicalTreeLifecycle, IBehavior, StyledElementAction, StyledElementActionImpl};
5use ferroui_base::data::converters::IValueConverter;
6use ferroui_base::data::core::ValueType;
7use ferroui_base::input::{ICommand, InputElement};
8use ferroui_base::utilities::CultureInfo;
9use ferroui_base::{
10    ferro_class, ferro_class_info, ferro_properties, BoxedValue, DirectProperty, FerroObjectExtensions, FerroObjectImpl,
11    FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty,
12};
13use std::cell::{Cell, OnceCell};
14use std::rc::Rc;
15
16/// Command action base class: an action that invokes a command, reports whether the command can
17/// execute ([`can_execute_command`](Self::can_execute_command)) and can make the input element of
18/// its trigger follow it
19/// ([`use_command_can_execute_for_is_enabled`](Self::use_command_can_execute_for_is_enabled)).
20///
21/// The command is observed while the action is in the logical tree of its trigger.
22///
23/// The class is abstract: a deriving class implements
24/// [`StyledElementActionImpl::execute`].
25#[repr(C)]
26pub struct InvokeCommandActionBase {
27    base: StyledElementAction,
28    command_can_execute_observer: OnceCell<CommandCanExecuteObserver>,
29    command_can_execute_is_enabled_binder: CommandCanExecuteIsEnabledBinder,
30    can_execute_command: Cell<bool>,
31    pass_event_args_to_command: Cell<bool>,
32}
33
34ferro_class!(InvokeCommandActionBase: StyledElementAction);
35ferro_class_info!(InvokeCommandActionBase {
36    markup: {
37        properties: [
38            PassEventArgsToCommand: bool {
39                get: |this: &Ref<InvokeCommandActionBase>| this.pass_event_args_to_command(),
40                set: |this: &Ref<InvokeCommandActionBase>, value: bool| this.set_pass_event_args_to_command(value)
41            },
42        ],
43    },
44});
45ferroui_base::ferro_impl_classes!(InvokeCommandActionBase: StyledElementImpl, StyledElementActionImpl);
46
47ferro_properties! {
48    impl InvokeCommandActionBase {
49        /// Identifies the `Command` property.
50        pub fn command_property() -> StyledProperty<Option<Rc<dyn ICommand>>> {
51            FerroProperty::register::<InvokeCommandActionBase, _>("Command", None)
52        }
53
54        /// Identifies the `CanExecuteCommand` property.
55        pub fn can_execute_command_property() -> DirectProperty<InvokeCommandActionBase, bool> {
56            FerroProperty::register_direct::<InvokeCommandActionBase, _>(
57                "CanExecuteCommand",
58                |action| action.can_execute_command(),
59                None,
60                false,
61            )
62        }
63
64        /// Identifies the `CanExecuteCommandParameter` property.
65        pub fn can_execute_command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
66            FerroProperty::register::<InvokeCommandActionBase, _>("CanExecuteCommandParameter", None)
67        }
68
69        /// Identifies the `UseCommandCanExecuteForIsEnabled` property.
70        pub fn use_command_can_execute_for_is_enabled_property() -> StyledProperty<bool> {
71            FerroProperty::register::<InvokeCommandActionBase, _>("UseCommandCanExecuteForIsEnabled", false)
72        }
73
74        /// Identifies the `CommandParameter` property.
75        pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
76            FerroProperty::register::<InvokeCommandActionBase, _>("CommandParameter", None)
77        }
78
79        /// Identifies the `InputConverter` property.
80        pub fn input_converter_property() -> StyledProperty<Option<Rc<dyn IValueConverter>>> {
81            FerroProperty::register::<InvokeCommandActionBase, _>("InputConverter", None)
82        }
83
84        /// Identifies the `InputConverterParameter` property.
85        pub fn input_converter_parameter_property() -> StyledProperty<Option<BoxedValue>> {
86            FerroProperty::register::<InvokeCommandActionBase, _>("InputConverterParameter", None)
87        }
88
89        /// Identifies the `InputConverterLanguage` property. The empty string of the default
90        /// value means the invariant culture.
91        pub fn input_converter_language_property() -> StyledProperty<Option<String>> {
92            FerroProperty::register::<InvokeCommandActionBase, _>("InputConverterLanguage", Some(String::new()))
93        }
94    }
95}
96
97ferroui_base::ferro_overrides! {
98    impl FerroObjectImpl for InvokeCommandActionBase {
99        /// The constructor body: the observer reports to the `CanExecuteCommand` property.
100        fn constructed(this: &Self) {
101            Self::parent_constructed(this);
102
103            let action = this.to_ref().downgrade();
104            let _ = this.command_can_execute_observer.set(CommandCanExecuteObserver::new(move |value| {
105                if let Some(action) = action.upgrade() {
106                    action.set_can_execute_command(value);
107                }
108            }));
109        }
110
111        fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
112            Self::parent_on_property_changed(this, change);
113
114            let property = change.property();
115            if property == Self::command_property().as_property()
116                || property == Self::command_parameter_property().as_property()
117                || property == Self::can_execute_command_parameter_property().as_property()
118                || property == Self::input_converter_property().as_property()
119            {
120                this.update_command_can_execute_observer();
121            } else if property == Self::use_command_can_execute_for_is_enabled_property().as_property() {
122                this.update_command_can_execute_is_enabled_binding();
123            }
124        }
125    }
126}
127
128impl InvokeCommandActionBase {
129    /// The static constructor: the class takes part in the logical tree of its trigger.
130    fn static_constructor() {
131        <dyn IActionLogicalTreeLifecycle>::register::<InvokeCommandActionBase>();
132    }
133
134    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
135    pub fn construct() -> Self {
136        Self {
137            base: StyledElementAction::construct(),
138            command_can_execute_observer: OnceCell::new(),
139            command_can_execute_is_enabled_binder: CommandCanExecuteIsEnabledBinder::new(),
140            can_execute_command: Cell::new(true),
141            pass_event_args_to_command: Cell::new(false),
142        }
143    }
144
145    /// Gets or sets the command this action should invoke.
146    pub fn command(&self) -> Option<Rc<dyn ICommand>> {
147        self.get_value(Self::command_property())
148    }
149
150    pub fn set_command(&self, value: Option<Rc<dyn ICommand>>) {
151        self.set_value(Self::command_property(), value)
152    }
153
154    /// Gets a value indicating whether [`command`](Self::command) can execute with the current
155    /// can-execute parameter.
156    pub fn can_execute_command(&self) -> bool {
157        self.can_execute_command.get()
158    }
159
160    fn set_can_execute_command(&self, value: bool) {
161        self.set_and_raise_cell(Self::can_execute_command_property(), &self.can_execute_command, value);
162    }
163
164    /// Gets or sets the parameter that is passed to the `can_execute` of the command. When it is
165    /// not set, [`command_parameter`](Self::command_parameter) is used if it is set. It does not
166    /// change the parameter passed to the `execute` of the command.
167    pub fn can_execute_command_parameter(&self) -> Option<BoxedValue> {
168        self.get_value(Self::can_execute_command_parameter_property())
169    }
170
171    pub fn set_can_execute_command_parameter(&self, value: Option<BoxedValue>) {
172        self.set_value(Self::can_execute_command_parameter_property(), value)
173    }
174
175    /// Gets or sets a value indicating whether the control associated with the hosting trigger
176    /// should have its `IsEnabled` property follow [`can_execute_command`](Self::can_execute_command).
177    pub fn use_command_can_execute_for_is_enabled(&self) -> bool {
178        self.get_value(Self::use_command_can_execute_for_is_enabled_property())
179    }
180
181    pub fn set_use_command_can_execute_for_is_enabled(&self, value: bool) {
182        self.set_value(Self::use_command_can_execute_for_is_enabled_property(), value)
183    }
184
185    /// Gets or sets the parameter that is passed to the `execute` of the command. If this is not
186    /// set, the parameter given to the action is used.
187    pub fn command_parameter(&self) -> Option<BoxedValue> {
188        self.get_value(Self::command_parameter_property())
189    }
190
191    pub fn set_command_parameter(&self, value: Option<BoxedValue>) {
192        self.set_value(Self::command_parameter_property(), value)
193    }
194
195    /// Gets or sets the converter that is run on the parameter given to the action.
196    pub fn input_converter(&self) -> Option<Rc<dyn IValueConverter>> {
197        self.get_value(Self::input_converter_property())
198    }
199
200    pub fn set_input_converter(&self, value: Option<Rc<dyn IValueConverter>>) {
201        self.set_value(Self::input_converter_property(), value)
202    }
203
204    /// Gets or sets the parameter that is passed to the `convert` of the
205    /// [`input_converter`](Self::input_converter).
206    pub fn input_converter_parameter(&self) -> Option<BoxedValue> {
207        self.get_value(Self::input_converter_parameter_property())
208    }
209
210    pub fn set_input_converter_parameter(&self, value: Option<BoxedValue>) {
211        self.set_value(Self::input_converter_parameter_property(), value)
212    }
213
214    /// Gets or sets the language that is passed to the `convert` of the
215    /// [`input_converter`](Self::input_converter).
216    pub fn input_converter_language(&self) -> Option<String> {
217        self.get_value(Self::input_converter_language_property())
218    }
219
220    pub fn set_input_converter_language(&self, value: Option<String>) {
221        self.set_value(Self::input_converter_language_property(), value)
222    }
223
224    /// Specifies whether the event args of the event that triggered this action should be passed
225    /// to the command as a parameter.
226    pub fn pass_event_args_to_command(&self) -> bool {
227        self.pass_event_args_to_command.get()
228    }
229
230    pub fn set_pass_event_args_to_command(&self, value: bool) {
231        if self.pass_event_args_to_command.get() == value {
232            return;
233        }
234
235        self.pass_event_args_to_command.set(value);
236        self.update_command_can_execute_observer();
237    }
238
239    fn update_command_can_execute_observer(&self) {
240        if let Some(observer) = self.command_can_execute_observer.get() {
241            observer.update_with(self.command(), self.resolve_can_execute_parameter(), self.is_can_execute_parameter_known());
242        }
243    }
244
245    /// Resolves the command parameter that will be passed to the command: the
246    /// [`command_parameter`](Self::command_parameter) if it is set, else the parameter given to
247    /// the action converted by the [`input_converter`](Self::input_converter), else the parameter
248    /// itself if [`pass_event_args_to_command`](Self::pass_event_args_to_command).
249    pub fn resolve_parameter(&self, parameter: Option<&BoxedValue>) -> Option<BoxedValue> {
250        if self.is_set(Self::command_parameter_property()) {
251            self.command_parameter()
252        } else if let Some(input_converter) = self.input_converter() {
253            let culture = match self.input_converter_language() {
254                Some(language) => CultureInfo::get_culture_info(&language),
255                None => CultureInfo::current_culture(),
256            };
257            input_converter
258                .convert(parameter, ValueType::object(), self.input_converter_parameter().as_ref(), &culture)
259                .ok()
260                .flatten()
261        } else if self.pass_event_args_to_command() {
262            parameter.cloned()
263        } else {
264            None
265        }
266    }
267
268    fn resolve_can_execute_parameter(&self) -> Option<BoxedValue> {
269        if self.is_set(Self::can_execute_command_parameter_property()) {
270            return self.can_execute_command_parameter();
271        }
272
273        if self.is_set(Self::command_parameter_property()) {
274            self.command_parameter()
275        } else {
276            None
277        }
278    }
279
280    fn is_can_execute_parameter_known(&self) -> bool {
281        self.is_set(Self::can_execute_command_parameter_property())
282            || self.is_set(Self::command_parameter_property())
283            || (self.input_converter().is_none() && !self.pass_event_args_to_command())
284    }
285
286    fn update_command_can_execute_is_enabled_binding(&self) {
287        self.command_can_execute_is_enabled_binder.update(
288            self.resolve_command_can_execute_is_enabled_target(),
289            self.use_command_can_execute_for_is_enabled(),
290            self.get_observable(Self::can_execute_command_property()),
291        );
292    }
293
294    /// The input element the action blocks: the input element a behavior among the action and
295    /// its logical ancestors is attached to, or the first input element among them.
296    fn resolve_command_can_execute_is_enabled_target(&self) -> Option<Ref<InputElement>> {
297        for logical in self.get_self_and_logical_ancestors() {
298            if let Some(behavior) = <dyn IBehavior>::of(&logical) {
299                if let Some(associated_input_element) =
300                    behavior.associated_object().and_then(|associated_object| associated_object.cast::<InputElement>())
301                {
302                    return Some(associated_input_element);
303                }
304            }
305
306            if let Some(input_element) = logical.cast::<InputElement>() {
307                return Some(input_element);
308            }
309        }
310
311        None
312    }
313}
314
315impl IActionLogicalTreeLifecycle for InvokeCommandActionBase {
316    fn attached_to_action_logical_tree(&self) {
317        if let Some(observer) = self.command_can_execute_observer.get() {
318            observer.start_with(self.command(), self.resolve_can_execute_parameter(), self.is_can_execute_parameter_known());
319        }
320        self.update_command_can_execute_is_enabled_binding();
321    }
322
323    fn detached_from_action_logical_tree(&self) {
324        self.command_can_execute_is_enabled_binder.stop();
325        if let Some(observer) = self.command_can_execute_observer.get() {
326            observer.stop();
327        }
328    }
329}