ferroui_reactiveui_controls/
view_model_view_host.rs1use crate::navigation_target::NavigationTarget;
5use crate::reactive_view::{IReactiveViewClass, ReactiveViewFor};
6use crate::view_host_navigation::{log_info, ViewHostNavigation};
7use crate::view_model_value::view_model_of_optional;
8use ferroui_base::input::InputElementImpl;
9use ferroui_base::interactivity::InteractiveImpl;
10use ferroui_base::layout::LayoutableImpl;
11use ferroui_base::{
12 ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, BoxedValue, FerroObject,
13 FerroObjectExtensions, FerroObjectImpl, FerroProperty, Ref, StaticType, StyledElementImpl, StyledProperty, TypeInfo,
14 VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
15};
16use ferroui_controls::primitives::TemplatedControlImpl;
17use ferroui_controls::{ContentControlImpl, ControlImpl, TransitioningContentControl};
18use ferroui_reactive::ObservableExt;
19use ferroui_reactiveui::{IViewFor, IViewLocator};
20use ferroui_splat::IEnableLogger;
21use ferroui_system::IDisposable;
22use std::any::Any;
23use std::cell::RefCell;
24use std::rc::Rc;
25
26#[repr(C)]
37pub struct ViewModelViewHost {
38 base: TransitioningContentControl,
39 view_locator: RefCell<Option<Rc<dyn IViewLocator>>>,
41 navigation_disposables: RefCell<Option<Rc<dyn IDisposable>>>,
44}
45
46ferro_class!(ViewModelViewHost: TransitioningContentControl);
47ferro_class_info!(ViewModelViewHost { new: ViewModelViewHost::new });
48ferro_impl_classes!(
49 ViewModelViewHost: FerroObjectImpl,
50 LayoutableImpl,
51 InteractiveImpl,
52 InputElementImpl,
53 ControlImpl,
54 TemplatedControlImpl,
55 ContentControlImpl
56);
57
58impl IEnableLogger for ViewModelViewHost {}
59
60impl StyledElementImpl for ViewModelViewHost {
61 fn style_key_override(_this: &Self) -> &'static TypeInfo {
63 <TransitioningContentControl as StaticType>::TYPE
64 }
65}
66
67impl VisualImpl for ViewModelViewHost {
68 fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
71 if this.navigation_disposables.borrow().is_some() {
72 return;
73 }
74 let disposables = this.create_navigation_disposables(e);
75 *this.navigation_disposables.borrow_mut() = Some(disposables);
76 }
77
78 fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
80 Self::parent_on_detached_from_visual_tree(this, e);
81 this.dispose_navigation_disposables();
82 }
83}
84
85ferro_properties! {
86 impl ViewModelViewHost {
87 pub fn view_model_property() -> StyledProperty<Option<BoxedValue>> {
89 FerroProperty::register::<ViewModelViewHost, _>("ViewModel", None)
90 }
91
92 pub fn view_contract_property() -> StyledProperty<Option<String>> {
94 FerroProperty::register::<ViewModelViewHost, _>("ViewContract", None)
95 }
96
97 pub fn default_content_property() -> StyledProperty<Option<BoxedValue>> {
99 FerroProperty::register::<ViewModelViewHost, _>("DefaultContent", None)
100 }
101 }
102}
103
104impl ViewModelViewHost {
105 pub fn construct() -> Self {
107 Self {
108 base: TransitioningContentControl::construct(),
109 view_locator: RefCell::new(None),
110 navigation_disposables: RefCell::new(None),
111 }
112 }
113
114 pub fn new() -> Ref<Self> {
116 instantiate(Self::construct())
117 }
118
119 pub fn view_model(&self) -> Option<BoxedValue> {
121 self.get_value(Self::view_model_property())
122 }
123
124 pub fn set_view_model(&self, value: Option<BoxedValue>) {
126 self.set_value(Self::view_model_property(), value)
127 }
128
129 pub fn view_contract(&self) -> Option<String> {
131 self.get_value(Self::view_contract_property())
132 }
133
134 pub fn set_view_contract(&self, value: Option<String>) {
136 self.set_value(Self::view_contract_property(), value)
137 }
138
139 pub fn default_content(&self) -> Option<BoxedValue> {
141 self.get_value(Self::default_content_property())
142 }
143
144 pub fn set_default_content(&self, value: Option<BoxedValue>) {
146 self.set_value(Self::default_content_property(), value)
147 }
148
149 pub fn view_locator(&self) -> Option<Rc<dyn IViewLocator>> {
151 self.view_locator.borrow().clone()
152 }
153
154 pub fn set_view_locator(&self, value: Option<Rc<dyn IViewLocator>>) {
156 *self.view_locator.borrow_mut() = value;
157 }
158
159 pub fn view_for(this: &Ref<Self>) -> Rc<dyn IViewFor> {
161 ReactiveViewFor::new(this.clone())
162 }
163
164 pub fn navigate_to_view_model(&self, view_model: Option<Rc<dyn Any>>, contract: Option<&str>) {
171 let Some(view_model) = view_model else {
172 log_info(self, "ViewModel is null. Falling back to default content.");
173 self.set_content(self.default_content());
174 return;
175 };
176
177 let content = ViewHostNavigation::resolve_view(self, view_model, contract, self.view_locator())
178 .map(|view| ViewHostNavigation::content_of_view(&view))
179 .or_else(|| self.default_content());
180 self.set_content(content);
181 }
182
183 pub fn dispose_navigation_disposables(&self) {
186 ViewHostNavigation::release(&self.navigation_disposables);
187 }
188
189 fn create_navigation_disposables(&self, e: &VisualTreeAttachmentEventArgs) -> Rc<dyn IDisposable> {
192 Self::parent_on_attached_to_visual_tree(self, e);
193
194 let object: &FerroObject = self;
195 let view_contract = FerroObjectExtensions::get_observable(object, Self::view_contract_property());
196 let navigation = FerroObjectExtensions::get_observable(object, Self::view_model_property())
197 .combine_latest(view_contract, |view_model: Option<BoxedValue>, contract: Option<String>| {
198 NavigationTarget::new(view_model_of_optional(view_model.as_ref()), contract)
199 });
200
201 let weak = self.to_ref().downgrade();
202 ViewHostNavigation::subscribe(navigation, move |view_model, contract| {
203 if let Some(this) = weak.upgrade() {
204 this.navigate_to_view_model(view_model, contract.as_deref());
205 }
206 })
207 }
208}
209
210impl IReactiveViewClass for ViewModelViewHost {
211 fn view_model_property() -> &'static StyledProperty<Option<BoxedValue>> {
212 ViewModelViewHost::view_model_property()
213 }
214}