Where to put initialization and saving logic

Topics: Questions
Nov 2, 2012 at 8:54 AM

Hello,

here I have a very basic question.
I have a window which allows the user to set some filter properties.
The last used properties should be saved and loaded on window close/open.

The following classes are created:

FilterModel : DataObjectBase
  - stores the filter properties

FilterViewModel : ViewModelBase
  - provides the model via a [Model] property
  - provides additional logic

FilterView : CatelUserControl
 - binds directly to the model properties through the viewmodels Model property.

 

For the loading, inside the FilterViewModel constructor the Model instance is created/loaded and the Model property is set.
So loading seems okay to me.

My problem is with saving.
The model has to be stored when the view/viewmodel is closing.
I cannot find an appropriate hook besides the viewmodels destructor to start the saving.

Sure, I could save the model on each PropertyChange event, but this seems too often to me.

Thanks, Alex.

Coordinator
Nov 2, 2012 at 3:21 PM

You are aware of the Save and Initialize methods on the view model? They are automatically called when the window is closed (via OK, otherwise the Cancel method is called). Close is always called.

Initialize is called when the view is loaded.

Nov 3, 2012 at 9:54 AM

Hi Geert,

I have no clue why, but the OnUnloaded event of my UserControls is raised just after creating the control and the VM.
After this, a new VM is created and assigned, but on this one, Close() is never called... 

Alex. 

Coordinator
Nov 4, 2012 at 11:10 AM

This might be possible in case of tabs or other controls. As soon as a view gets disconnected from the visual tree, it is unloaded. A tab that is not visible is directly removed from the visual tree.

Can you provide a *good working* repro? We have not had any other issues with this before so I hardly doubt whether it is an issue in Catel.

Nov 5, 2012 at 9:40 AM

Okay, found the root cause.
My view resides within a dockingmanager control (like AvalonDock, but from infragistics).
This control has the ability to save and restore its layout. When restoring the layout, all controls are unloaded and reloaded....

So, I've thought about it and came to the following solution:
I need to do some action on the viewmodel of  the usercontrol when the root window is closing.
To reach this goal, I've coded a bahavior which executes a vm command on the windows event.

I think this behavior may be useful in general, so I've coded and documented in the Catel's style.
There is a base class allowing easy implementation for other window events, too.
Find the code attached.

The usage is simple: place the behavior inside a usercontrol and an Command inside its viewmodel.
Pay attention to place the behavior within the root element of the usercontrol and not directly on the usercontrol itself.
If you place it on the usercontrol, the binding to the command will not work. (@Geert: why?) 

Alex.

 

using System;
using System.ComponentModel;
using System.Windows;

namespace Catel.Windows.Interactivity
{
    /// <summary>
    /// Behavior base class that catches an event from the root window element.<br />
    /// The event is forwarded to the DataContext of the <see cref="FrameworkElement"/> it is attached to.
    /// </summary>
    public abstract class WindowEventToCommandBehaviorBase
        : Catel.Windows.Interactivity.CommandBehaviorBase<FrameworkElement>
    {
        #region Fields
        /// <summary>
        /// Stores a reference to the window the event handler is registered on.<br />
        /// Will be used to deregister the event handler.
        /// </summary>
        private Window _currentWindow = null;

        /// <summary>
        /// Will be executed instead of the command if set.
        /// </summary>
        private readonly Action<Window> _action;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="WindowEventToCommandBehaviorBase"/> class.
        /// </summary>
        protected WindowEventToCommandBehaviorBase()
            : this(null) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="WindowEventToCommandBehaviorBase"/> class.
        /// </summary>
        /// <param name="action">The action to execute on double click. This is very useful when the behavior is added
        /// via code and an action must be invoked instead of a command.</param>
        protected WindowEventToCommandBehaviorBase(Action<Window> action)
        {
            _action = action;
        }
        #endregion

        #region Methods
        /// <summary>
        /// Called when the associated object is loaded.<br />
        /// Registers the event subscription.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        protected override void OnAssociatedObjectLoaded(object sender, EventArgs e)
        {
            UnsubscribeFromWindowEvent();
            SubscribeToWindowEvent();
        }

        /// <summary>
        /// Called when the associated object is unloaded.
        /// Deregisters the event subscription.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        protected override void OnAssociatedObjectUnloaded(object sender, EventArgs e)
        {
            UnsubscribeFromWindowEvent();
        }

        /// <summary>
        /// Registers the handler to the event of the current window.
        /// </summary>
        private void SubscribeToWindowEvent()
        {
            var window = Window.GetWindow(AssociatedObject);
            if (window == null) return;
            RegisterEventHandler(window);
            this._currentWindow = window;
        }

        /// <summary>
        /// Removes the handler registration from the previous window.
        /// </summary>
        private void UnsubscribeFromWindowEvent()
        {
            if (this._currentWindow == null) return;
            UnregisterEventHandler(this._currentWindow);
            this._currentWindow = null;
        }

        /// <summary>
        /// Registers the handler to the event of the given window.
        /// <code>
        /// protected override void RegisterEventHandler(Window window)
        /// {
        ///     window.Closing += WindowOnClosing;
        /// }
        /// 
        /// private void WindowOnClosing(object sender, CancelEventArgs cancelEventArgs)
        /// {
        ///     ExecuteCommand(sender);
        /// }
        /// </code>
        /// </summary>
        /// <param name="window">The window instance the eventhandler has to be registered to.</param>
        protected abstract void RegisterEventHandler(Window window);

        /// <summary>
        /// Unregisters the handler from the event of the given window.
        /// <code>
        /// protected override void RegisterEventHandler(Window window)
        /// {
        ///     window.Closed += WindowOnClosed;
        /// }
        /// 
        /// private void WindowOnClosed(object sender, EventArgs eventArgs)
        /// {
        ///     ExecuteCommand(sender);
        /// }
        /// </code>
        /// </summary>
        /// <param name="window">The window instance the eventhandler has to be unregistered from.</param>
        protected abstract void UnregisterEventHandler(Window window);

        /// <summary>
        /// Invokes the Action or executes the Command.<br />
        /// The current window instance is used as parameter.
        /// </summary>
        protected override void ExecuteCommand()
        {
            this.ExecuteCommand(this._currentWindow);
        }

        /// <summary>
        /// Invokes the Action or executes the Command.
        /// The given window instance is used as parameter.
        /// </summary>
        protected override void ExecuteCommand(object parameter)
        {
            var window = parameter as Window;
            if (_action != null)
            {
                _action(window);
            }
            else
            {
                base.ExecuteCommand(window);
            }
        }
        #endregion
    }

    /// <summary>
    /// This behavior allows catching of the containing windows closing event within the viewmodel by forwarding it to a Command.<br />
    /// The behavior has to be added to the root element of an UserControl.
    /// </summary>
    public class WindowClosingEventToCommand : WindowEventToCommandBehaviorBase
    {
        /// <summary>
        /// Registers the handler to the event of the given window.
        /// </summary>
        protected override void RegisterEventHandler(Window window)
        {
            window.Closing += ContainingWindowOnClosing;
        }

        /// <summary>
        /// Unregisters the handler from the event of the given window.
        /// </summary>
        protected override void UnregisterEventHandler(Window window)
        {
            window.Closing -= ContainingWindowOnClosing;
        }

        /// <summary>
        /// Handles the windows OnClosing event and executes the behaviors action/command.
        /// </summary>
        private void ContainingWindowOnClosing(object sender, CancelEventArgs cancelEventArgs)
        {
            ExecuteCommand(sender);
        }
    }

    /// <summary>
    /// This behavior allows catching of the containing windows closed event within the viewmodel by forwarding it to a Command.
    /// The behavior has to be added to the root element of an UserControl.
    /// </summary>
    public class WindowClosedEventToCommand : WindowEventToCommandBehaviorBase
    {
        /// <summary>
        /// Registers the handler to the event of the given window.
        /// </summary>
        protected override void RegisterEventHandler(Window window)
        {
            window.Closed += ContainingWindowOnClosed;
        }

        /// <summary>
        /// Unregisters the handler from the event of the given window.
        /// </summary>
        protected override void UnregisterEventHandler(Window window)
        {
            window.Closed -= ContainingWindowOnClosed;
        }

        /// <summary>
        /// Handles the windows OnClosed event and executes the behaviors action/command.
        /// </summary>
        private void ContainingWindowOnClosed(object sender, EventArgs eventArgs)
        {
            ExecuteCommand(sender);
        }
    }
}
 

Coordinator
Nov 7, 2012 at 8:22 AM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.
Coordinator
Nov 7, 2012 at 5:30 PM

Excellent code, thank you for your contribution. I will release a new version via nuget tonight which will include this.

Nov 8, 2012 at 5:27 AM
Edited Nov 8, 2012 at 5:34 AM

@Geert

Can you explain why the command binding for the behavior does not work if the behavior is placed directly within the UserControl in XAML?
I have to add it to a nested element to get it working.
It seems, the datacontext of the UserControl itself is not the viewmodel, but the view itself. 

Alex.

btw: I like the changes you've done to allow subscribing to any window event by name :-)

Coordinator
Nov 8, 2012 at 6:34 AM

That's correct. Catel adds a layer between the control and the content to use as a placeholder for the datacontext (viewmodel). This way, Catel can still respond to changes of the real datacontext and change the vm according to that.