Skip to main content

ferroui_xaml_interactions/core/
call_method_action.rs

1//! Port of `Core/CallMethodAction.cs`.
2
3use ferroui_base::{
4    ferro_class, ferro_class_info, ferro_properties, instantiate, BoxedValue, FerroObjectImpl, FerroObjectImplExt,
5    FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty,
6};
7use ferroui_xaml_interactivity::helpers::member_lookup::type_name_of;
8use ferroui_xaml_interactivity::helpers::MethodHelper;
9use ferroui_xaml_interactivity::{StyledElementAction, StyledElementActionImpl};
10use std::rc::Rc;
11
12/// An action that calls a method on a specified object when invoked.
13///
14/// The method is a public method of the target that takes no parameters, or two parameters of
15/// which the first is the sender; it is found by name in the markup metadata of the type of the
16/// target (see [`MethodHelper`]).
17#[repr(C)]
18pub struct CallMethodAction {
19    base: StyledElementAction,
20    method_helper: MethodHelper,
21}
22
23ferro_class!(CallMethodAction: StyledElementAction);
24ferro_class_info!(CallMethodAction {
25    new: CallMethodAction::new,
26    markup: {
27        property_attributes: [
28            TargetObject: [ResolveByName],
29        ],
30    },
31});
32ferroui_base::ferro_impl_classes!(CallMethodAction: StyledElementImpl);
33
34ferro_properties! {
35    impl CallMethodAction {
36        /// Identifies the `MethodName` property.
37        pub fn method_name_property() -> StyledProperty<Option<String>> {
38            FerroProperty::register::<CallMethodAction, _>("MethodName", None)
39        }
40
41        /// Identifies the `TargetObject` property.
42        pub fn target_object_property() -> StyledProperty<Option<BoxedValue>> {
43            FerroProperty::register::<CallMethodAction, _>("TargetObject", None)
44        }
45    }
46}
47
48ferroui_base::ferro_overrides! {
49    impl FerroObjectImpl for CallMethodAction {
50        fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
51            Self::parent_on_property_changed(this, change);
52
53            if change.property() == Self::method_name_property().as_property() {
54                let method_name = change.get_new_value::<Option<String>>();
55                this.method_helper.update_method_descriptors(method_name.as_deref());
56            }
57
58            if change.property() == Self::target_object_property().as_property() {
59                let new_value = change.get_new_value::<Option<BoxedValue>>();
60                if let Some(new_value) = new_value {
61                    this.method_helper.update_target_type(&new_value, this.method_name().as_deref());
62                }
63            }
64        }
65    }
66}
67
68impl StyledElementActionImpl for CallMethodAction {
69    /// Executes the action.
70    ///
71    /// `sender` is the object that is passed to the action by the behavior. Generally this is the
72    /// associated object of the behavior or a target object. The value of `parameter` is
73    /// determined by the caller.
74    ///
75    /// Returns true if the method is called; else false.
76    ///
77    /// # Panics
78    /// Panics when [`target_object`](CallMethodAction::target_object) is set and has no method of
79    /// the name with the expected signature (the argument exception of the reference).
80    fn execute(this: &Self, sender: Option<&BoxedValue>, parameter: Option<&BoxedValue>) -> Option<BoxedValue> {
81        if !this.is_enabled() {
82            return Some(Rc::new(false));
83        }
84
85        let Some(method_name) = this.method_name().filter(|name| !name.is_empty()) else {
86            return Some(Rc::new(false));
87        };
88
89        let target = match this.target_object() {
90            Some(target_object) => Some(target_object),
91            None => sender.cloned(),
92        };
93        let Some(target) = target else {
94            return Some(Rc::new(false));
95        };
96
97        this.method_helper.update_target_type(&target, Some(&method_name));
98
99        let Some(method_descriptor) = this.method_helper.find_best_method(parameter) else {
100            if this.target_object().is_some() {
101                panic!(
102                    "Cannot find method named {} on object of type {} that matches the expected signature.",
103                    method_name,
104                    type_name_of(&target)
105                );
106            }
107
108            return Some(Rc::new(false));
109        };
110
111        let method = method_descriptor.method_info();
112        let result = match method_descriptor.parameter_count() {
113            0 => (method.invoke)(&[Some(target.clone())]),
114            2 => (method.invoke)(&[Some(target.clone()), Some(target.clone()), parameter.cloned()]),
115            _ => return Some(Rc::new(false)),
116        };
117
118        // The exception of the invoked method propagates (`TargetInvocationException`).
119        if let Err(error) = result {
120            panic!("{error}");
121        }
122
123        Some(Rc::new(true))
124    }
125}
126
127impl CallMethodAction {
128    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
129    pub fn construct() -> Self {
130        Self { base: StyledElementAction::construct(), method_helper: MethodHelper::new() }
131    }
132
133    /// Creates an action.
134    pub fn new() -> Ref<Self> {
135        instantiate(Self::construct())
136    }
137
138    /// Gets or sets the name of the method to invoke. This is a property of the object model.
139    pub fn method_name(&self) -> Option<String> {
140        self.get_value(Self::method_name_property())
141    }
142
143    pub fn set_method_name(&self, value: Option<String>) {
144        self.set_value(Self::method_name_property(), value)
145    }
146
147    /// Gets or sets the object that exposes the method of interest. Markup resolves a name given
148    /// for it to the named element. This is a property of the object model.
149    pub fn target_object(&self) -> Option<BoxedValue> {
150        self.get_value(Self::target_object_property())
151    }
152
153    pub fn set_target_object(&self, value: Option<BoxedValue>) {
154        self.set_value(Self::target_object_property(), value)
155    }
156}