Skip to main content

ferroui_reactiveui_controls/
view_model_view_host.rs

1//! `ViewModelViewHost` (`ViewModelViewHost.cs`): loads and displays the view associated with the
2//! `ViewModel` property.
3
4use 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/// Loads and displays the view associated with the `ViewModel` property (`ViewModelViewHost`).
27///
28/// Assigning a view model resolves its view through the view locator ([`view_locator`] or the
29/// current one) under the [`view_contract`]; the view gets the view model through its `ViewModel`
30/// and `DataContext`. Without a view model, or without a view for it, the host shows its
31/// [`default_content`].
32///
33/// [`view_locator`]: Self::view_locator
34/// [`view_contract`]: Self::view_contract
35/// [`default_content`]: Self::default_content
36#[repr(C)]
37pub struct ViewModelViewHost {
38    base: TransitioningContentControl,
39    /// The view locator used to resolve views (`ViewLocator`); `None` uses the current one.
40    view_locator: RefCell<Option<Rc<dyn IViewLocator>>>,
41    /// The active navigation subscriptions while the host is attached to the visual tree
42    /// (`_navigationDisposables`).
43    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    /// `StyleKeyOverride`: the host is styled as a `TransitioningContentControl`.
62    fn style_key_override(_this: &Self) -> &'static TypeInfo {
63        <TransitioningContentControl as StaticType>::TYPE
64    }
65}
66
67impl VisualImpl for ViewModelViewHost {
68    /// `OnAttachedToVisualTree`: creates the navigation subscriptions once (`??=`); the base
69    /// handler runs when they are created.
70    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    /// `OnDetachedFromVisualTree`.
79    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        /// Identifies the `ViewModel` property (`ViewModelProperty`).
88        pub fn view_model_property() -> StyledProperty<Option<BoxedValue>> {
89            FerroProperty::register::<ViewModelViewHost, _>("ViewModel", None)
90        }
91
92        /// Identifies the `ViewContract` property (`ViewContractProperty`).
93        pub fn view_contract_property() -> StyledProperty<Option<String>> {
94            FerroProperty::register::<ViewModelViewHost, _>("ViewContract", None)
95        }
96
97        /// Identifies the `DefaultContent` property (`DefaultContentProperty`).
98        pub fn default_content_property() -> StyledProperty<Option<BoxedValue>> {
99            FerroProperty::register::<ViewModelViewHost, _>("DefaultContent", None)
100        }
101    }
102}
103
104impl ViewModelViewHost {
105    /// Field initialisation.
106    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    /// Creates the host.
115    pub fn new() -> Ref<Self> {
116        instantiate(Self::construct())
117    }
118
119    /// The view model whose view is shown (`ViewModel`).
120    pub fn view_model(&self) -> Option<BoxedValue> {
121        self.get_value(Self::view_model_property())
122    }
123
124    /// Sets the view model whose view is shown.
125    pub fn set_view_model(&self, value: Option<BoxedValue>) {
126        self.set_value(Self::view_model_property(), value)
127    }
128
129    /// The contract of the view to resolve (`ViewContract`).
130    pub fn view_contract(&self) -> Option<String> {
131        self.get_value(Self::view_contract_property())
132    }
133
134    /// Sets the contract of the view to resolve.
135    pub fn set_view_contract(&self, value: Option<String>) {
136        self.set_value(Self::view_contract_property(), value)
137    }
138
139    /// The content shown when there is no view model or no view for it (`DefaultContent`).
140    pub fn default_content(&self) -> Option<BoxedValue> {
141        self.get_value(Self::default_content_property())
142    }
143
144    /// Sets the content shown when there is no view model or no view for it.
145    pub fn set_default_content(&self, value: Option<BoxedValue>) {
146        self.set_value(Self::default_content_property(), value)
147    }
148
149    /// The view locator used to resolve views (`ViewLocator`); `None` uses the current one.
150    pub fn view_locator(&self) -> Option<Rc<dyn IViewLocator>> {
151        self.view_locator.borrow().clone()
152    }
153
154    /// Sets the view locator used to resolve views.
155    pub fn set_view_locator(&self, value: Option<Rc<dyn IViewLocator>>) {
156        *self.view_locator.borrow_mut() = value;
157    }
158
159    /// The host as a view of ReactiveUI (`(IViewFor)host`), which keeps it alive.
160    pub fn view_for(this: &Ref<Self>) -> Rc<dyn IViewFor> {
161        ReactiveViewFor::new(this.clone())
162    }
163
164    /// Navigates to the view associated with `view_model` and `contract` (`NavigateToViewModel`):
165    /// a missing view model or view displays the default content; a resolved view receives the
166    /// view model through its `ViewModel` and `DataContext`.
167    ///
168    /// Deviation (DEVIATIONS.md, ReactiveUI controls): internal upstream, public here so that the
169    /// tests, in another module, reach it.
170    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    /// Disposes the active navigation subscriptions when they exist
184    /// (`DisposeNavigationDisposables`).
185    pub fn dispose_navigation_disposables(&self) {
186        ViewHostNavigation::release(&self.navigation_disposables);
187    }
188
189    /// Creates the active navigation subscriptions for an attached host
190    /// (`CreateNavigationDisposables`): the base handler runs first.
191    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}