Heavy lifting after first OnLayoutUpdated

Oct 13, 2010 at 5:25 AM
Edited Oct 13, 2010 at 5:27 AM

The WP7 Performance guidelines recommend that one does heavy lifting after first layoutupdated event fires after the page has been navigated to. There is a code sample which i've based on the below code on in the document.

I'm using CM, but have found that I need to do this in the code behind of the view as follows. I would appreciate it if someone could recommend an alternative to this, or perhaps whether this concept can be brought into CM. 

I've used this to good effect and it ensures that the page being navigated to actually appears to the user before the heavy lifting is done. If you rely on OnActivate override in a VM that inherits from Screen this does not happen.


    private bool _onNavigatedToCalled = false;

        #region Public Constructor

        public BusinessView()
        {
            InitializeComponent();
            LayoutUpdated += new EventHandler(View_LayoutUpdated);
        }

        #endregion

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            _onNavigatedToCalled = true;
        }

        void View_LayoutUpdated(object sender, EventArgs e)
        {
            if (_onNavigatedToCalled)
            {
                _onNavigatedToCalled = false;
                Dispatcher.BeginInvoke(() =>
                {
                    ((BusinessViewModel)this.DataContext).AfterLayoutFirstUpdated();
                }
                );
            }
        }
Oct 13, 2010 at 10:23 PM

Keith we saw the same guidance. I did some logging to map out the View and Page load life-cycle. The skinny: you will override your ViewModel's OnViewLoaded for any heavy lifting. It is declared in Screen

public class Screen : PropertyChangedBase, IScreen, IChild<IConductor>, IViewAware
{
... 
protected virtual void OnViewLoaded(object view) {})
...
... and is called right after the Page's Loaded and LayoutUpdated would have be called. I would think twice (maybe thrice) before using the code-behind of a View for anything. Probably best to delete the ".CS" to remove the temptation.

Here's a full logging of the life-cycle:

[Caliburn.Micro.ActionMessage] - Invoking Action: GotoPageThree.
[Caliburn.Micro.HelloWP7.PageThree] - Constructor
[Caliburn.Micro.Screen] - Constructor
[Caliburn.Micro.ViewModelBinder] - Binding Caliburn.Micro.HelloWP7.PageThree and Caliburn.Micro.HelloWP7.PageThreeViewModel.
[Caliburn.Micro.Action] - Setting DC of Caliburn.Micro.HelloWP7.PageThree to Caliburn.Micro.HelloWP7.PageThreeViewModel.
[Caliburn.Micro.Action] - Attaching message handler Caliburn.Micro.HelloWP7.PageThreeViewModel to Caliburn.Micro.HelloWP7.PageThree.
[Caliburn.Micro.ViewModelBinder] - Attaching Caliburn.Micro.HelloWP7.PageThree to Caliburn.Micro.HelloWP7.PageThreeViewModel.
[Caliburn.Micro.Screen] - AttachView
[Caliburn.Micro.Screen] - PageThreeViewModel_ViewAttached
[Caliburn.Micro.ViewModelBinder] - No bindable control for action CanClose.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action AttachView.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action GetView.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action get_Parent.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action set_Parent.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action get_DisplayName.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action set_DisplayName.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action get_IsActive.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action get_IsInitialized.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action add_Activated.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action remove_Activated.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action add_AttemptingDeactivation.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action remove_AttemptingDeactivation.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action add_Deactivated.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action remove_Deactivated.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action add_ViewAttached.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action remove_ViewAttached.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action TryClose.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action add_PropertyChanged.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action remove_PropertyChanged.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action get_IsNotifying.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action set_IsNotifying.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action Refresh.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action NotifyOfPropertyChange.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action RaisePropertyChangedEventImmediately.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action ToString.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action Equals.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action GetHashCode.
[Caliburn.Micro.ViewModelBinder] - No bindable control for action GetType.
[Caliburn.Micro.ViewModelBinder] - No convention applied to LayoutRoot.
[Caliburn.Micro.ViewModelBinder] - No convention applied to TitlePanel.
[Caliburn.Micro.ViewModelBinder] - No convention applied to ContentPanel.
[Caliburn.Micro.ViewModelBinder] - No convention applied to ApplicationTitle.
[Caliburn.Micro.ViewModelBinder] - No convention applied to PageTitle.
[Caliburn.Micro.Screen] - OnInitialize
[Caliburn.Micro.Screen] - Activating Caliburn.Micro.HelloWP7.PageThreeViewModel.
[Caliburn.Micro.Screen] - OnActivate
[Caliburn.Micro.Screen] - PageThreeViewModel_Activated
[Caliburn.Micro.HelloWP7.PageThree] - PageThree_LayoutUpdated
[Caliburn.Micro.HelloWP7.PageThree] - PageThree_Loaded
[Caliburn.Micro.Screen] - OnViewLoaded

Hope this helps.

Oct 13, 2010 at 10:49 PM

If Loaded is too late and you really need to do work after the View's LayoutUpdated but before the View's Loaded then you'll need to modify Screen slightly (see ">>" below). 

Rob, you up for adding a hook for OnLayoutUpdated?

 

    /// <summary
    /// Attaches a view to this instance.
    /// </summary>
    /// <param name="view">The view.</param>
    /// <param name="context">The context in which the view appears.</param>
    public virtual void AttachView(object view, object context)
    {
        var loadWired = views.Values.Contains(view);
        views[context ?? ViewLocator.DefaultContext] = view;

        var element = view as FrameworkElement;
        if(!loadWired && element != null)
            element.Loaded += delegate { OnViewLoaded(view); };

>>      if (!loadWired && element != null)
>>          element.LayoutUpdated += delegate { OnLayoutUpdated(view); };

        if(!loadWired)
            ViewAttached(this, new ViewAttachedEventArgs{ View = view, Context = context });
    }

>>  /// <summary>
>>  /// Called when an attached view's LayoutUpdated event fires.
>>  /// </summary>
>>  /// <param name="view"></param>
>>  protected virtual void OnLayoutUpdated(object view) { }

 

 

Oct 14, 2010 at 12:48 AM

Hi, 

Yeah, thanks for that. What we really need I think is an OnAfterLayoutFirstUpdated (or something was a better name perhaps;) which is called once after the LayoutUpdated is called after the page has been navigated to, this is what that annoying boiler plate code in the view CB is doing. 

According to the performance doc, Loaded or NavigatedTo by themselves are not ideal places to kick off heavy lifting. If Screen were to expose such a method that internalised the type of boiler plate code to get the responsiveness benefits that would be ideal. 

I'm not quite familiar enough with the CM Internals to do this myself at this stage unfortunately, thought it might be easy for someone a bit more knowledgable.

Oct 14, 2010 at 12:51 AM

i should have pointed out that the only problem i think with your last example is that we can hook into LayoutUpdated (i an do this with an action message on the view even!), the issue is hooking into OnNavigatedTo, which isn't AFAIK exposed as an event hook, just as an override in the page. So this is what initially made it hard for me to move the code into the VM because of the seeming need to hook into OnNavigatedTo and set that boolean so that OnLayoutUpdated was only used the first time after the page had been navigated to.

Oct 14, 2010 at 4:52 AM
Edited Oct 14, 2010 at 4:54 AM

I might be missing something (happens often), but is there anything significant about the OnNavigateTo event? 

I think the real idea in the MSDN sample is to wait to do "big work" until the first frame has rendered, and then at that point kick off heavy work asynchronously.  They're wishing to avoid apps that appear to freeze the phone (ex-- user clicks the "Show XyzView" button, the screen freezes for five seconds while webservices are hit, and finally the XyzView is displayed). At LayoutUpdated you'll know the first frame has been rendered so the sample writer picked that spot (the earliest responsible spot) for their sample, and since LayoutUpdated can fire many times in the life of a page they had to add a "already done it" guard to keep the heaviest work from happening repeatedly (not a great way to "create high performing applications"-- hee hee). 

I believe for most scenarios you can safely wait for Caliburn's Screen::OnViewLoaded and then kick off your async heavy lifting there. The folks writing the MSDN samples are pitching out possible solutions to particular problems. There's a danger ("cargo cult") in following/copying MSDN samples without questioning and understanding the "why". I've been guilty of this with the WP7 stuff far too frequently myself.

Cheers,
Bryan 

Oct 18, 2010 at 2:58 AM

The article specifically says not to use the Loaded event handler because the xaml parser needs to parse all the xaml and instantiate all objects on the page which "can be time consuming". 

I'm assuming that CM's OnViewLoaded hooks off the Loaded event in which case it would be to late to take advantage of approach recommended by MS.

I have tested my code running off the loaded event versus the hook off first call on  OnLayoutUpdated after OnNavigatedTo and app is decidedly more responsive (otherwise there is a delay before the page actually appears it seems). 

There is nothing significant about OnNavigatedTo, other than it's a natural place to deal with the control logic. i.e. "do the heavy lifting after the page has been navigated to and the layout update event is fired for the first time". 

 

Coordinator
Oct 18, 2010 at 3:57 AM

You can override the Screen's AttachView method. That gets called when the View and ViewModel are bound together byt eh ViewModelBinder. You could use that as an opportunity to wire the LayoutUpdated event. That's basically what the base class does, but for the Loaded event.

Oct 18, 2010 at 4:01 AM

mmm, ok will give it a try. I'm not sure if the LayoutUpdated gets fired before OnNavigatedTo in which case it might be too early, but will give it a shot thanks. 

Coordinator
Oct 18, 2010 at 4:09 AM

Another idea would be to take the FrameAdapter and rework it so that it calls into your screen's class at the appropriate time. You could add a new interface to implement on screens that care about this, such as ILayoutAware or something like that.

Oct 18, 2010 at 4:34 AM

ah yes, a custom frameadapter and an interface, sounds like the best way.....thanks

Feb 8, 2011 at 2:56 PM

Just curious, has anything changed in the default CM code base to handle this scenario?

Coordinator
Feb 8, 2011 at 5:32 PM

Nothing has changed. If you definitely need to change this, I recommend that you go with the custom FrameAdapter and interface as descrived above. It's possible that it would be easier to customize the existing ViewModelBinder to do this as well. That's probably the better way.