Skip to main content

ferroui_xaml_interactions_custom/validation/
property_validation_behavior.rs

1//! Port of `Validation/PropertyValidationBehavior.cs` (`PropertyValidationBehavior<TControl, TValue>`).
2
3use super::contract::IValidationRule;
4use ferroui_base::collections::{FerroList, NotifyCollectionChangedEventArgs};
5use ferroui_base::data::core::ValueTypes;
6use ferroui_base::data::BindingMode;
7use ferroui_base::reactive::IDisposable;
8use ferroui_base::{
9    ferro_class, ferro_class_info, ferro_properties, instantiate, BoxedValue, DirectProperty, FerroObject,
10    FerroObjectImpl, FerroProperty, FerroPropertyChangedEventArgs, Ref, StaticType, StyledElementImpl,
11    StyledProperty, StyledPropertyOptions, TypeInfo, WeakRef,
12};
13use ferroui_controls::{Control, DataValidationErrors};
14use ferroui_xaml_interactivity::{
15    DisposableAction, DisposingBehavior, DisposingBehaviorImpl, StyledElementBehaviorImpl, StyledElementBehaviorImplExt,
16};
17use std::any::TypeId;
18use std::cell::{OnceCell, RefCell};
19use std::ops::Deref;
20use std::rc::Rc;
21
22/// The validation rules of a [`PropertyValidationBehavior`]: a list of objects that implement
23/// [`IValidationRule`].
24///
25/// Deviation (DEVIATIONS.md, Behaviors): the reference types the collection as the generic list of
26/// the framework of `IValidationRule<TValue>`; here it is a named list of objects of the class
27/// model (as `ActionCollection` of the interactivity crate), so that markup can add the rules of
28/// a document to it; an object that does not implement the contract is skipped by the validation.
29///
30/// It is a reference object: clones refer to the same collection and compare by identity.
31#[derive(Clone, PartialEq)]
32pub struct ValidationRules(FerroList<Ref<FerroObject>>);
33
34impl ValidationRules {
35    /// Creates an empty collection.
36    pub fn new() -> Self {
37        Self(FerroList::new())
38    }
39
40    /// The list of the collection.
41    pub fn list(&self) -> &FerroList<Ref<FerroObject>> {
42        &self.0
43    }
44}
45
46impl Default for ValidationRules {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl Deref for ValidationRules {
53    type Target = FerroList<Ref<FerroObject>>;
54
55    fn deref(&self) -> &FerroList<Ref<FerroObject>> {
56        &self.0
57    }
58}
59
60impl std::fmt::Debug for ValidationRules {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(f, "ValidationRules({})", self.0.count())
63    }
64}
65
66/// The rules a behavior subscribed to, with the tokens of their handlers.
67type SubscribedRules = RefCell<Vec<(Ref<FerroObject>, u64)>>;
68
69/// The handler of the property changes of the rules.
70type RulePropertyChanged = Rc<dyn Fn(&FerroPropertyChangedEventArgs<'_>)>;
71
72/// The line separator of the joined error messages (`Environment.NewLine`).
73const NEW_LINE: &str = if cfg!(windows) { "\r\n" } else { "\n" };
74
75/// Base behavior that validates a property value using a set of rules.
76///
77/// Deviation (DEVIATIONS.md, Behaviors): the reference class is generic over the type of the
78/// associated control (`TControl`) and of the validated property (`TValue`); classes of the class
79/// model are not generic, so both are runtime types given at creation
80/// ([`new_of`](Self::new_of)`::<TControl, TValue>()`, [`construct`](Self::construct)): the associated
81/// object is checked against the first as the reference does, and the property is validated only
82/// when its value type is the second (`Property is FerroProperty<TValue>` of the reference).
83/// The rules receive the value untyped (see [`IValidationRule`]).
84///
85/// Deviation (DEVIATIONS.md, Behaviors): markup cannot give a class of the object model type
86/// arguments (`x:TypeArguments="TextBox, sys:String"`), so the behavior markup creates
87/// ([`new`](Self::new)) is attached to any object and validates the property
88/// whatever the type of its value: the `PropertyValidationBehavior<FerroObject, object>` of the
89/// reference, where every property is a property of `object`.
90#[repr(C)]
91pub struct PropertyValidationBehavior {
92    base: DisposingBehavior,
93    /// The type of the value of the validated property, or `None` for any type.
94    value_type: Option<TypeId>,
95    rules: OnceCell<ValidationRules>,
96}
97
98ferro_class!(PropertyValidationBehavior: DisposingBehavior);
99ferro_class_info!(PropertyValidationBehavior {
100    new: PropertyValidationBehavior::new,
101    markup: {
102        content: Rules,
103    },
104});
105ferroui_base::ferro_impl_classes!(PropertyValidationBehavior: FerroObjectImpl, StyledElementImpl);
106
107ferro_properties! {
108    impl PropertyValidationBehavior {
109        /// Identifies the `Property` property.
110        pub fn property_property() -> StyledProperty<Option<&'static FerroProperty>> {
111            FerroProperty::register::<PropertyValidationBehavior, _>("Property", None)
112        }
113
114        /// Identifies the `IsValid` property.
115        pub fn is_valid_property() -> StyledProperty<bool> {
116            FerroProperty::register_with::<PropertyValidationBehavior, _>(
117                "IsValid",
118                StyledPropertyOptions::new(true).default_binding_mode(BindingMode::TwoWay),
119            )
120        }
121
122        /// Identifies the `Error` property.
123        pub fn error_property() -> StyledProperty<Option<String>> {
124            FerroProperty::register::<PropertyValidationBehavior, _>("Error", None)
125        }
126
127        /// Identifies the `Rules` property.
128        pub fn rules_property() -> DirectProperty<PropertyValidationBehavior, ValidationRules> {
129            FerroProperty::register_direct::<PropertyValidationBehavior, _>(
130                "Rules",
131                |behavior| behavior.rules(),
132                None,
133                ValidationRules::new(),
134            )
135        }
136    }
137}
138
139ferroui_base::ferro_overrides! {
140    impl DisposingBehaviorImpl for PropertyValidationBehavior {
141        fn on_attached_override(this: &Self) -> Rc<dyn IDisposable> {
142            this.subscribe()
143        }
144    }
145}
146
147ferroui_base::ferro_overrides! {
148    impl StyledElementBehaviorImpl for PropertyValidationBehavior {
149        fn on_loaded(this: &Self) {
150            Self::parent_on_loaded(this);
151
152            this.validate();
153        }
154    }
155}
156
157impl PropertyValidationBehavior {
158    /// Creates the class data of a behavior attached to objects of `associated_type` (the
159    /// `TControl` of the reference) that validates a property whose value is of type `value_type`
160    /// (the `TValue` of the reference); see [`ferroui_base::FerroObject::construct`].
161    pub fn construct(associated_type: &'static TypeInfo, value_type: TypeId) -> Self {
162        Self { base: DisposingBehavior::construct(associated_type), value_type: Some(value_type), rules: OnceCell::new() }
163    }
164
165    /// Creates the class data of a behavior attached to any object that validates a property
166    /// whatever the type of its value (the behavior of markup, [`new`](Self::new)).
167    pub fn construct_untyped() -> Self {
168        Self { base: DisposingBehavior::construct(FerroObject::TYPE), value_type: None, rules: OnceCell::new() }
169    }
170
171    /// Creates a behavior attached to objects of class `TControl` that validates a property whose
172    /// value is of type `TValue` (`new PropertyValidationBehavior<TControl, TValue>()`).
173    pub fn new_of<TControl: StaticType, TValue: 'static>() -> Ref<Self> {
174        instantiate(Self::construct(TControl::TYPE, TypeId::of::<TValue>()))
175    }
176
177    /// Creates a behavior attached to any object that validates a property whatever the type of
178    /// its value (`new PropertyValidationBehavior<FerroObject, object>()` of the reference, the
179    /// form markup creates: `<PropertyValidationBehavior Property="{x:Static TextBox.TextProperty}">`).
180    pub fn new() -> Ref<Self> {
181        instantiate(Self::construct_untyped())
182    }
183
184    /// Gets or sets the property to validate. This is a property of the object model.
185    pub fn property(&self) -> Option<&'static FerroProperty> {
186        self.get_value(Self::property_property())
187    }
188
189    pub fn set_property(&self, value: Option<&'static FerroProperty>) {
190        self.set_value(Self::property_property(), value)
191    }
192
193    /// Gets validation rules collection. This is a property of the object model, and the content
194    /// of the behavior in markup.
195    pub fn rules(&self) -> ValidationRules {
196        self.rules.get_or_init(ValidationRules::new).clone()
197    }
198
199    /// Gets or sets value indicating whether the property value is valid. This is a property of
200    /// the object model.
201    pub fn is_valid(&self) -> bool {
202        self.get_value(Self::is_valid_property())
203    }
204
205    pub fn set_is_valid(&self, value: bool) {
206        self.set_value(Self::is_valid_property(), value)
207    }
208
209    /// Gets or sets the validation error message. This is a property of the object model.
210    pub fn error(&self) -> Option<String> {
211        self.get_value(Self::error_property())
212    }
213
214    pub fn set_error(&self, value: Option<String>) {
215        self.set_value(Self::error_property(), value)
216    }
217
218    /// The property to validate, when its value is of the type the behavior validates.
219    fn validated_property(&self) -> Option<&'static FerroProperty> {
220        self.property().filter(|property| self.value_type.is_none_or(|value_type| property.property_type() == value_type))
221    }
222
223    /// `OnAttachedOverride` of the reference: validates when the property of the associated
224    /// object, a rule or the collection of the rules changes.
225    fn subscribe(&self) -> Rc<dyn IDisposable> {
226        let Some(associated_object) = self.associated_object() else {
227            return DisposableAction::empty();
228        };
229
230        let Some(property) = self.validated_property() else {
231            return DisposableAction::empty();
232        };
233
234        let rules = self.rules();
235        let behavior: WeakRef<PropertyValidationBehavior> = self.to_ref().downgrade();
236        let subscribed_rules: Rc<SubscribedRules> = Rc::new(RefCell::new(Vec::new()));
237
238        let rule_property_changed: RulePropertyChanged = {
239            let behavior = behavior.clone();
240            Rc::new(move |_e: &FerroPropertyChangedEventArgs<'_>| {
241                if let Some(behavior) = behavior.upgrade() {
242                    behavior.validate();
243                }
244            })
245        };
246
247        let rules_collection_changed = {
248            let behavior = behavior.clone();
249            let subscribed_rules = subscribed_rules.clone();
250            let rule_property_changed = rule_property_changed.clone();
251            move |_e: &NotifyCollectionChangedEventArgs<'_, Ref<FerroObject>>| {
252                Self::detach_rules(&subscribed_rules);
253
254                if let Some(behavior) = behavior.upgrade() {
255                    for rule in behavior.rules().snapshot().iter() {
256                        Self::attach_rule(&subscribed_rules, rule, &rule_property_changed);
257                    }
258
259                    behavior.validate();
260                }
261            }
262        };
263
264        let handler = {
265            let behavior = behavior.clone();
266            associated_object.property_changed(move |e: &FerroPropertyChangedEventArgs<'_>| {
267                if e.property() == property {
268                    if let Some(behavior) = behavior.upgrade() {
269                        let value = e.to_owned_args().new_value().and_then(ValueTypes::normalize);
270                        behavior.validate_value(value.as_ref());
271                    }
272                }
273            })
274        };
275        let rules_token = rules.add_collection_changed(Rc::new(rules_collection_changed));
276
277        for rule in rules.snapshot().iter() {
278            Self::attach_rule(&subscribed_rules, rule, &rule_property_changed);
279        }
280
281        DisposableAction::create(move || {
282            handler.dispose();
283            rules.remove_collection_changed(rules_token);
284            Self::detach_rules(&subscribed_rules);
285        })
286    }
287
288    /// Subscribes to the property changes of `rule`, once.
289    fn attach_rule(subscribed_rules: &SubscribedRules, rule: &Ref<FerroObject>, handler: &RulePropertyChanged) {
290        let mut subscribed_rules = subscribed_rules.borrow_mut();
291        if subscribed_rules.iter().any(|(subscribed, _)| subscribed.ptr_eq(rule)) {
292            return;
293        }
294
295        let token = rule.add_property_changed_handler(handler.clone());
296        subscribed_rules.push((rule.clone(), token));
297    }
298
299    /// Ends the subscriptions to the property changes of the rules.
300    fn detach_rules(subscribed_rules: &SubscribedRules) {
301        let subscribed = std::mem::take(&mut *subscribed_rules.borrow_mut());
302        for (rule, token) in subscribed {
303            rule.remove_property_changed_handler(token);
304        }
305    }
306
307    /// Validates the current value of the property.
308    fn validate(&self) {
309        let (Some(associated_object), Some(property)) = (self.associated_object(), self.validated_property()) else {
310            return;
311        };
312
313        let value = ValueTypes::normalize(associated_object.get_value_untyped(property));
314        self.validate_value(value.as_ref());
315    }
316
317    /// Validates `value` with every rule, and publishes the result and the error messages (also
318    /// as the data validation errors of the associated control).
319    fn validate_value(&self, value: Option<&BoxedValue>) {
320        let mut errors: Vec<String> = Vec::new();
321        let mut valid = true;
322
323        for rule in self.rules().snapshot().iter() {
324            let Some(rule) = <dyn IValidationRule>::of(rule) else {
325                continue;
326            };
327
328            if !rule.validate(value) {
329                valid = false;
330                if let Some(error_message) = rule.error_message().filter(|message| !message.is_empty()) {
331                    errors.push(error_message);
332                }
333            }
334        }
335
336        self.set_is_valid(valid);
337        self.set_error(if errors.is_empty() { None } else { Some(errors.join(NEW_LINE)) });
338
339        if let Some(control) = self.associated_object_as::<Control>() {
340            let errors: Option<Vec<BoxedValue>> = if errors.is_empty() {
341                None
342            } else {
343                Some(errors.into_iter().map(|error| Rc::new(error) as BoxedValue).collect())
344            };
345            DataValidationErrors::set_errors(&control, errors);
346        }
347    }
348}