ferroui_xaml_interactions_custom/validation/
property_validation_behavior.rs1use 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#[derive(Clone, PartialEq)]
32pub struct ValidationRules(FerroList<Ref<FerroObject>>);
33
34impl ValidationRules {
35 pub fn new() -> Self {
37 Self(FerroList::new())
38 }
39
40 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
66type SubscribedRules = RefCell<Vec<(Ref<FerroObject>, u64)>>;
68
69type RulePropertyChanged = Rc<dyn Fn(&FerroPropertyChangedEventArgs<'_>)>;
71
72const NEW_LINE: &str = if cfg!(windows) { "\r\n" } else { "\n" };
74
75#[repr(C)]
91pub struct PropertyValidationBehavior {
92 base: DisposingBehavior,
93 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 pub fn property_property() -> StyledProperty<Option<&'static FerroProperty>> {
111 FerroProperty::register::<PropertyValidationBehavior, _>("Property", None)
112 }
113
114 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 pub fn error_property() -> StyledProperty<Option<String>> {
124 FerroProperty::register::<PropertyValidationBehavior, _>("Error", None)
125 }
126
127 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 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 pub fn construct_untyped() -> Self {
168 Self { base: DisposingBehavior::construct(FerroObject::TYPE), value_type: None, rules: OnceCell::new() }
169 }
170
171 pub fn new_of<TControl: StaticType, TValue: 'static>() -> Ref<Self> {
174 instantiate(Self::construct(TControl::TYPE, TypeId::of::<TValue>()))
175 }
176
177 pub fn new() -> Ref<Self> {
181 instantiate(Self::construct_untyped())
182 }
183
184 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 pub fn rules(&self) -> ValidationRules {
196 self.rules.get_or_init(ValidationRules::new).clone()
197 }
198
199 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 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 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 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 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 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 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 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}