Screen implementation

Dec 2, 2010 at 11:44 AM
Edited Dec 3, 2010 at 11:18 AM

I have a couple of questions and proposals regarding the Caliburn.Micro default screen implementation.

First of all, I noticed that the class contains a register of attached views. I understand the purpose of such a register (the IViewAware interface requires to know the view associated to the view-model), but the current implementation keeps the view alive even if removed from the visual tree. Could it be possible to implement the register as

readonly Dictionary<object, object> views = new Dictionary<object, WeakReference>();

so that the view object lifecycle (froim the GC point of view) is no longer than actually needed?

A second question is about the fact that Screen implements IChild<IConductor>. This implies that every screen should be conducted but, in my opinion, it is not always true (infact the IScreen interface do not enforces this dependency). Would it be possible to change the current implementation so that Screen inherits from IChild<object> (or better use a non-generic interface IChild as I suggest here) and check if the parent is an IConductor explicitly in the TryClose method? This way the Screen class could be general enough to be inherited in more or less every possible scenario (it would be a generic view-model implementing most of the framework capabilities without enforcing strong dependencies on other aspects of the framework itself).

It would be even possible to create a version that enforces the dependency from an IConductor (something like Screen.Conducted).

Following the Screen implementation with the changes discussed above:

namespace Caliburn.Micro
{
    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Windows;

    /// <summary>
    /// A base implementation of <see cref="IScreen"/>.
    /// </summary>
    public class Screen : PropertyChangedBase, IScreen, IChild, IViewAware
    {
        /// <summary>
        /// The log object.
        /// </summary>
        protected static readonly ILog Log = LogManager.GetLog(typeof(Screen));

        bool isActive;
        bool isInitialized;
        object parent;
        string displayName;
        readonly Dictionary<object, WeakReference> views = new Dictionary<object, WeakReference>();

        /// <summary>
        /// Creates an instance of the screen.
        /// </summary>
        public Screen()
        {
            DisplayName = GetType().FullName;
        }

        /// <summary>
        /// Gets or Sets the Parent.
        /// </summary>
        public object Parent
        {
            get { return GetParent(); }
            set { SetParent(value); }
        }

        /// <summary>
        /// Gets or Sets the Display Name
        /// </summary>
        public string DisplayName
        {
            get { return displayName; }
            set
            {
                displayName = value;
                NotifyOfPropertyChange("DisplayName");
            }
        }

        /// <summary>
        /// Indicates whether or not this instance is currently active.
        /// </summary>
        public bool IsActive
        {
            get { return isActive; }
            private set
            {
                isActive = value;
                NotifyOfPropertyChange("IsActive");
            }
        }

        /// <summary>
        /// Indicates whether or not this instance is currently initialized.
        /// </summary>
        public bool IsInitialized
        {
            get { return isInitialized; }
            private set
            {
                isInitialized = value;
                NotifyOfPropertyChange("IsInitialized");
            }
        }

        /// <summary>
        /// Raised after activation occurs.
        /// </summary>
        public event EventHandler<ActivationEventArgs> Activated = delegate { };

        /// <summary>
        /// Raised before deactivation.
        /// </summary>
        public event EventHandler<DeactivationEventArgs> AttemptingDeactivation = delegate { };

        /// <summary>
        /// Raised after deactivation.
        /// </summary>
        public event EventHandler<DeactivationEventArgs> Deactivated = delegate { };

        void IActivate.Activate()
        {
            if(IsActive)
                return;

            var initialized = false;

            if(!IsInitialized)
            {
                IsInitialized = initialized = true;
                OnInitialize();
            }

            IsActive = true;
            Log.Info("Activating {0}.", this);
            OnActivate();

            Activated(this, new ActivationEventArgs {
                WasInitialized = initialized
            });
        }

        /// <summary>
        /// Called when initializing.
        /// </summary>
        protected virtual void OnInitialize() {}

        /// <summary>
        /// Called when activating.
        /// </summary>
        protected virtual void OnActivate() {}

        void IDeactivate.Deactivate(bool close)
        {
            if(!IsActive && !IsInitialized)
                return;

            AttemptingDeactivation(this, new DeactivationEventArgs {
                WasClosed = close
            });

            IsActive = false;
            Log.Info("Deactivating {0}.", this);
            OnDeactivate(close);

            if(close)
                Log.Info("Closed {0}.", this);

            Deactivated(this, new DeactivationEventArgs {
                WasClosed = close
            });
        }

        /// <summary>
        /// Gets the parent.
        /// </summary>
        /// <returns>The parent.</returns>
        protected object GetParent()
        {
            return parent;
        }

        /// <summary>
        /// Sets the parent.
        /// </summary>
        /// <param name="value">The value.</param>
        protected virtual void SetParent(object value)
        {
            parent = value;
            NotifyOfPropertyChange("Parent");
        }

        /// <summary>
        /// Called when deactivating.
        /// </summary>
        /// <param name="close">Inidicates whether this instance will be closed.</param>
        protected virtual void OnDeactivate(bool close) {}

        /// <summary>
        /// Called to check whether or not this instance can close.
        /// </summary>
        /// <param name="callback">The implementor calls this action with the result of the close check.</param>
        public virtual void CanClose(Action<bool> callback)
        {
            callback(true);
        }

        /// <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.Any( x => x.Target == view);
            views[context ?? ViewLocator.DefaultContext] = new WeakReference(view);

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

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

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

        /// <summary>
        /// Gets a view previously attached to this instance.
        /// </summary>
        /// <param name="context">The context denoting which view to retrieve.</param>
        /// <returns>The view.</returns>
        public virtual object GetView(object context)
        {
            WeakReference reference;
            views.TryGetValue(context ?? ViewLocator.DefaultContext, out reference);
            return reference != null ? reference.Target : null;
        }

        /// <summary>
        /// Raised when a view is attached.
        /// </summary>
        public event EventHandler<ViewAttachedEventArgs> ViewAttached = delegate { };

        /// <summary>
        /// Tries to close this instance by asking its Parent to initiate shutdown or by asking its corresponding default view to close.
        /// </summary>
        public void TryClose() {
            Execute.OnUIThread(() =>
                               {
                IConductor conductor = Parent as IConductor;
                if(conductor != null)
                    conductor.CloseItem(this);
                else {
                    var view = GetView(null);

                    if(view == null) {
                        var ex = new NotSupportedException("A Parent or default view is required.");
                        Log.Error(ex);
                        throw ex;
                    }

                    var method = view.GetType().GetMethod("Close");
                    if(method != null) {
                        method.Invoke(view, null);
                        return;
                    }

                    var property = view.GetType().GetProperty("IsOpen");
                    if(property != null) {
                        property.SetValue(view, false, new object[] {});
                        return;
                    }

                    var ex2 = new NotSupportedException("The default view does not support Close/IsOpen.");
                    Log.Error(ex2);
                    throw ex2;
                }
            });
        }

#if !SILVERLIGHT

        /// <summary>
        /// Closes this instance by asking its Parent to initiate shutdown or by asking it's corresponding default view to close.
        /// This overload also provides an opportunity to pass a dialog result to it's corresponding default view.
        /// </summary>
        /// <param name="dialogResult">The dialog result.</param>
        public virtual void TryClose(bool? dialogResult)
        {
            Execute.OnUIThread(() => {
                var view = GetView(null);

                if(view != null)
                {
                    var property = view.GetType().GetProperty("DialogResult");
                    if(property != null)
                        property.SetValue(view, dialogResult, null);
                }

                TryClose();
            });
        }

#endif

        /// <summary>
        /// A base implementation for a conducted <see cref="IScreen"/>.
        /// </summary>
        public class Conducted : Screen, IChild<IConductor>
        {
            /// <summary>
            /// Gets or Sets the Parent.
            /// </summary>
            /// <value></value>
            public new IConductor Parent { get { return (IConductor)GetParent(); } set { SetParent(value); } }

            /// <summary>
            /// Sets the parent.
            /// </summary>
            /// <param name="value">The value.</param>
            protected override void SetParent(object value)
            {
                if (parent is IConductor)
                    base.SetParent(value);
                else
                    throw new InvalidOperationException("The parent of a conducted screen must be a conductor");
            }
        }
    }
}

 

Edit: Two slight modifications are needed in the ConductorBase and Conductor<T>.Collection.OneActive to avoid to break compatibility.

ConductorBase 

        /// <summary>
        /// Ensures that an item is ready to be activated.
        /// </summary>
        /// <param name="newItem"></param>
        /// <returns>The item to be activated.</returns>
        protected virtual T EnsureItem(T newItem)
        {
            var node = newItem as IChild;
            if (node != null && node.Parent != this)
                node.Parent = this;

            return newItem;
        }
ConductorBase and Conductor<T>.Collection.OneActive 
        /// <summary>
        /// Initializes a new instance of the <see cref="Conductor&lt;T&gt;.Collection.OneActive"/> class.
        /// </summary>
        public OneActive()
        {
            items.CollectionChanged += (s, e) =>
            {
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                    case NotifyCollectionChangedAction.Replace:
                        e.NewItems.OfType<IChild>().Apply(x => x.Parent = this);
                        break;
                    case NotifyCollectionChangedAction.Reset:
                        items.OfType<IChild>().Apply(x => x.Parent = this);
                        break;
                }
            };
        }