A property control with a fallback

So I ran into a problem, or maybe a bug, while using the Property Control in a WebForms implementation.
I needed to have a backup value for a property. e.g. if you have an introduction property on a page and a description to be used in the MetaData, you might want to return the content of the introduction if the description is empty. So I added a check to the getter of my property, if its’s empty, return the value of another one.

Within EPiServer 7.+  the Property Control, just gets the value specified directly and doesn’t use the getter from your property.

So I created a PropertyControl where you can also specify the FallbackPropertyName. Unfortunately I could not just override a few methods, because of all the privates used within the control. So it’s the complete code from the original with some extra’s. If you set the FallbackPropertyName, it will render that property if no content has been entered in the property specified in the PropertName. When you edit it, you will edit the property specified in the PropertyName.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using EPiServer.Core;
using EPiServer.Editor;
using EPiServer.Framework.Serialization;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using EPiServer.Web.PropertyControls;
using EPiServer.Web.WebControls;

namespace EPiServer.Web.WebControls
{
    /// <summary>
    ///     WebControl for rendering page properties.
    /// </summary>
    /// <remarks>
    ///     The Property control is used on web forms and user controls and renders the value
    ///     of built-in or custom properties to the HTML stream.
    /// </remarks>
    /// <example>
    ///     The following example shows how to print the name of the page to HTML:
    ///     <code source="../CodeSamples/EPiServerNET/WebControls/PropertySamples.aspx" region="PrintHTML" lang="aspnet" />
    ///     <note>
    ///         The PropertyName attribute is not case sensitive.
    ///     </note>
    ///     <para>
    ///         For the Property control to be able to read the properties from the page, it needs to
    ///         be hosted on a web form or a control that implements the IPageSource interface. The
    ///         control will iterate through the control hierarchy looking for this interface, and when
    ///         it finds one it will use the CurrentPage property to read the information about the
    ///         specific built-in or custom property.
    ///     </para>
    ///     <para>
    ///         If you put a Property control inside a templated control like the PageList control, that
    ///         implements IPageSource, the Property control will use the CurrentPage property of the
    ///         template control instead. The PageList then points the Property control to the current
    ///         PageData object in its internal PageDataCollection. This is why the two following PageList
    ///         examples will print the same:
    ///     </para>
    ///     <code source="../CodeSamples/EPiServerNET/WebControls/PropertySamples.aspx" region="PageListSamples" lang="aspnet" />
    ///     You can also access the inner PropertyData object through the InnerProperty property, if
    ///     you need access to the raw value.
    /// </example>
    [DefaultProperty("PropertyName")]
    [ParseChildren(true)]
    [PersistChildren(false)]
    [ToolboxData("<{0}:PropertyFallback runat=\"server\" />")]
    public sealed class PropertyFallback : WebControl,
                                           INamingContainer,
                                           IPageSource,
                                           IPageControl,
                                           IContentControl,
                                           IContentSource,
                                           IPropertyControlsContainer
    {
      #region Fields

        private readonly PropertyEditorSettings _editorSettings = new PropertyEditorSettings();

        private readonly PropertyEditorSettings _overlaySettings = new PropertyEditorSettings();

        private readonly PropertyRenderSettings _renderSettings = new PropertyRenderSettings();

        private IContentSource _contentSource;

        private PropertyContext _currentContext;

        private PropertyData _fallbackPropertyData;

        private bool _isBound;

        private bool _isDirty;

        private IPageSource _pageSource;

        private PropertyData _propertyData;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        ///     Initializes a new instance of the <see cref="T:EPiServer.Libraries.WebForms.Controls.Property" /> class.
        /// </summary>
        /// <remarks>
        ///     This control requires that either the <see cref="P:EPiServer.Libraries.WebForms.Controls.Property.PropertyName" />
        ///     or the <see cref="P:EPiServer.Libraries.WebForms.Controls.Property.InnerProperty" />
        ///     property is set for it to work properly.
        /// </remarks>
        public PropertyFallback()
        {
            this.Editable = true;
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="T:EPiServer.Libraries.WebForms.Controls.Property" /> class with the
        ///     given <see cref="T:EPiServer.Core.PropertyData" /> instance.
        /// </summary>
        /// <param name="propertyData">The inner property data object.</param>
        public PropertyFallback(PropertyData propertyData)
            : this()
        {
            this._propertyData = propertyData;
        }

        #endregion

        #region Public Properties

        /// <summary>
        ///     Gets or sets the content source.
        /// </summary>
        /// <value>
        ///     The content source.
        /// </value>
        [Browsable(false)]
        public IContentSource ContentSource
        {
            get
            {
                return this._contentSource ?? (this._contentSource = this.GetContentSource());
            }
            set
            {
                if (this._contentSource == value)
                {
                    return;
                }

                this._propertyData = null;
                this._contentSource = value;
                this._isDirty = true;
            }
        }

        public override string CssClass
        {
            get
            {
                return this.RenderSettings.CssClass ?? string.Empty;
            }
            set
            {
                if (this.CssClass == value)
                {
                    return;
                }

                this.RenderSettings.CssClass = value;
                this._isDirty = true;
            }
        }

        /// <summary>
        ///     Gets the currently loaded <see cref="T:EPiServer.Core.IContent">content</see>.
        /// </summary>
        /// <value>
        ///     Returns information about the currently loaded content, or a content in
        ///     a collection when used inside a control.
        /// </value>
        public IContent CurrentContent
        {
            get
            {
                return this.ContentSource.CurrentContent ?? this.PageSource.CurrentPage;
            }
        }

        /// <summary>
        ///     Gets or sets the tag name. If not set a span-tag will be used.
        /// </summary>
        /// <example>
        ///     Set to "h1" to create a h1-tag around the content.
        /// </example>
        /// <value>
        ///     The custom tag name.
        /// </value>
        [Category("Appearance")]
        [DefaultValue("")]
        public string CustomTagName
        {
            get
            {
                return this.RenderSettings.CustomTag ?? string.Empty;
            }
            set
            {
                if (this.CustomTagName == value)
                {
                    return;
                }

                this.RenderSettings.CustomTag = value;
                this._isDirty = true;
            }
        }

        /// <summary>
        ///     Gets or sets a value indicating if an error message should be displayed if
        ///     no property with the name provided by <see cref="P:EPiServer.Libraries.WebForms.Controls.Property.PropertyName" />
        ///     could be found.
        /// </summary>
        /// <remarks>
        ///     If the property pointed to by PropertyName does not exist, an error message will be
        ///     printed instead. If you want to suppress this error message, set this property to false.
        ///     <code>
        /// <![CDATA[<episerver:property propertyname="MyProperty" runat="server" DisplayMissingMessage='false' />]]>
        /// </code>
        /// </remarks>
        [DefaultValue(true)]
        [Bindable(true)]
        [Category("Appearance")]
        public bool DisplayMissingMessage
        {
            get
            {
                return (bool)(this.ViewState["_displayMissingMessage"] ?? true);
            }
            set
            {
                this.ViewState["_displayMissingMessage"] = value;
            }
        }

        /// <summary>
        ///     Controls if the property should render it's edit mode.
        /// </summary>
        /// <remarks>
        ///     By setting the EditMode property to true the property will be rendered in "edit view",
        ///     just like it is when viewed through the Editors View.
        ///     <note>
        ///         <b>Note:</b>  Any changes to the edit control will be saved if you enter DOPE mode on
        ///         the page and save it, or call the OnDopeSave() client side method from client side script.
        ///     </note>
        ///     <code>
        /// <![CDATA[<episerver:property editmode="True" runat="server" propertyname="MainBody" />
        ///             <button onclick="OnDopeSave()">Save</button>]]>
        /// </code>
        /// </remarks>
        [Category("Behavior")]
        [DefaultValue(false)]
        public bool EditMode
        {
            get
            {
                return (bool)(this.ViewState["_isEditMode"] ?? false);
            }
            set
            {
                this.ViewState["_isEditMode"] = value;
            }
        }

        /// <summary>
        ///     Controls if the property is editable with DOPE
        /// </summary>
        /// <remarks>
        ///     The Property control will give you DOPE (Direct On Page Editing) support if the
        ///     underlaying template page supports it (any web form that inherits directly or
        ///     indirectly from TemplatePage. If you do not want DOPE support you can either inherit
        ///     from SimplePage, or set the Editable property to false, like this:
        ///     <code>
        /// <![CDATA[<episerver:property propertyname="PageName" runat="server" Editable='false' />]]>
        /// </code>
        /// </remarks>
        [DefaultValue(true)]
        [Bindable(true)]
        [Category("Behavior")]
        public bool Editable
        {
            get
            {
                return (bool)(this.ViewState["_isEditable"] ?? true) & this.AllowEditable();
            }
            set
            {
                this.ViewState["_isEditable"] = value;
            }
        }

        /// <summary>
        ///     Gets or sets any editor settings for the property.
        /// </summary>
        /// <value>
        ///     The editor settings for the property.
        /// </value>
        /// <remarks>
        ///     These settings can vary from property to property, consult the documentation for each property for more details on
        ///     custom properties.
        /// </remarks>
        [TypeConverter(typeof(JsonStringToPropertyEditorSettingsConverter))]
        [Category("Appearance")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [DefaultValue("")]
        public PropertyEditorSettings EditorSettings
        {
            get
            {
                return this._editorSettings;
            }
        }

        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("")]
        public string FallbackPropertyName
        {
            get
            {
                return
                    (string)
                    (this.ViewState["_fallbackPropertyName"]
                     ?? ((this._fallbackPropertyData != null) ? this._fallbackPropertyData.Name : string.Empty));
            }
            set
            {
                if (this.FallbackPropertyName == value)
                {
                    return;
                }

                this._fallbackPropertyData = null;
                this._isDirty = true;
                this.ViewState["_fallbackPropertyName"] = value;
            }
        }

        /// <summary>
        ///     Set or get the inner property used by this control
        /// </summary>
        [Browsable(false)]
        public PropertyData InnerProperty
        {
            get
            {
                this.InitializeInnerProperty();

                if (this._propertyData.Value != null)
                {
                    return this._propertyData;
                }

                this.InitializeInnerFallbackProperty();
                return this._fallbackPropertyData;
            }
            set
            {
                this.Editable = false;

                if (value != null)
                {
                    this.PropertyName = value.Name;
                }
                else
                {
                    this._currentContext = null;
                    this._isDirty = true;
                }

                this._propertyData = value;
            }
        }

        /// <summary>
        ///     Gets or sets any overlay settings for the property.
        /// </summary>
        /// <value>
        ///     The overlay settings for the property.
        /// </value>
        /// <remarks>
        ///     These settings can vary from property to property, consult the documentation for each property for more details on
        ///     custom properties.
        /// </remarks>
        [DefaultValue("")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Category("Appearance")]
        [TypeConverter(typeof(JsonStringToPropertyEditorSettingsConverter))]
        public PropertyEditorSettings OverlaySettings
        {
            get
            {
                return this._overlaySettings;
            }
        }

        /// <summary>
        ///     The root page to read data from if different from current
        /// </summary>
        /// <remarks>
        ///     If you do not want the Property web control to retrieve the property value from
        ///     the currently loaded page (or the current page in a PageDataCollection when inside
        ///     a templated web control) you can set the PageLink to point to another page.
        ///     <para>
        ///         The PageLink property is a PageReference, but you can also assign an int (the ID of the page.)
        ///     </para>
        ///     <para>
        ///         <code>
        /// <![CDATA[<episerver:property runat="server" propertyname="MainBody" PageLink="30" />]]>
        /// </code>
        ///     </para>
        ///     <para>
        ///         or:
        ///     </para>
        ///     <para>
        ///         <code>
        /// <![CDATA[<episerver:property runat="server" propertyname="MainBody" PageLink='<%# EPiServer.Global.EPConfig.StartPage%>' />]]>
        /// </code>
        ///     </para>
        /// </remarks>
        [DefaultValue(null)]
        public PageReference PageLink
        {
            get
            {
                return (PageReference)(this.ViewState["_pageLink"] ?? PageReference.EmptyReference);
            }
            set
            {
                if (this.PageLink == value)
                {
                    return;
                }

                this._propertyData = null;
                this._currentContext = null;
                this._isDirty = true;
                this.ViewState["_pageLink"] = value;
            }
        }

        /// <summary>
        ///     The property that contains the root page to read data from if different from current
        /// </summary>
        [DefaultValue(null)]
        public string PageLinkProperty
        {
            get
            {
                return (string)(this.ViewState["_pageLinkProperty"] ?? string.Empty);
            }
            set
            {
                if (this.PageLinkProperty == value)
                {
                    return;
                }

                this._propertyData = null;
                this._currentContext = null;
                this._isDirty = false;
                this.ViewState["_pageLinkProperty"] = value;
            }
        }

        /// <summary>
        ///     Return the IPageSource implementation that this property control uses to read page data.
        /// </summary>
        /// <value>
        ///     An IPageSource implementation.
        /// </value>
        /// <remarks>
        ///     The returned instance will usually be the base class for the aspx-page.
        /// </remarks>
        [Browsable(false)]
        public IPageSource PageSource
        {
            get
            {
                return this._pageSource ?? (this._pageSource = this.GetPageSource());
            }
            set
            {
                if (this._pageSource == value)
                {
                    return;
                }

                this._propertyData = null;
                this._pageSource = value;
                this._isDirty = true;
            }
        }

        /// <summary>
        ///     Gets or sets the name of the property that should be displayed by this control.
        /// </summary>
        /// <remarks>
        ///     Please note that the PropertyName attribute is not case sensitive.
        /// </remarks>
        /// <example>
        ///     The following example shows how to print the name of the page to HTML:
        ///     <code>
        /// <![CDATA[<episerver:property propertyname="PageName" runat="server" />]]>
        /// </code>
        /// </example>
        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("")]
        public string PropertyName
        {
            get
            {
                return
                    (string)
                    (this.ViewState["_propertyName"]
                     ?? (this._propertyData != null ? this._propertyData.Name : string.Empty));
            }
            set
            {
                if (this.PropertyName == value)
                {
                    return;
                }

                this._propertyData = null;
                this._isDirty = true;
                this.ViewState["_propertyName"] = value;
            }
        }

        /// <summary>
        ///     Gets or sets the scope name separator.
        /// </summary>
        /// <remarks>
        ///     The default value is '.'.
        /// </remarks>
        /// <value>
        ///     The scope name separator.
        /// </value>
        [DefaultValue('.')]
        public char PropertyScopeSeparator
        {
            get
            {
                return (char)(this.ViewState["_scopeNameSeparator"] ?? '.');
            }
            set
            {
                if (this.PropertyScopeSeparator == value)
                {
                    return;
                }

                this.ViewState["_scopeNameSeparator"] = value;
                this._isDirty = true;
            }
        }

        /// <summary>
        ///     The value of the loaded property
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        [Obsolete("Use InnerProperty.Value if you want to retrieve the value of the loaded property.")]
        public object PropertyValue
        {
            get
            {
                return this.InnerProperty != null ? this.InnerProperty.Value : null;
            }
        }

        /// <summary>
        ///     Gets or sets any render settings for the property.
        /// </summary>
        /// <value>
        ///     The render settings for the property.
        /// </value>
        /// <remarks>
        ///     These settings can vary from property to property, consult the documentation for each property for more details on
        ///     custom properties.
        /// </remarks>
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [DefaultValue("")]
        [Category("Appearance")]
        public PropertyRenderSettings RenderSettings
        {
            get
            {
                return this._renderSettings;
            }
        }

        /// <summary>
        ///     Gets or sets the validation group.
        /// </summary>
        /// <value>
        ///     The validation group.
        /// </value>
        [Category("Behavior")]
        [DefaultValue("")]
        public string ValidationGroup
        {
            get
            {
                return (string)(this.ViewState["_validationGroup"] ?? string.Empty);
            }
            set
            {
                this.ViewState["_validationGroup"] = value;
            }
        }

        #endregion

        #region Explicit Interface Properties

        PropertyContext IContentControl.CurrentContext
        {
            get
            {
                return this.CurrentContext;
            }
        }

        PageData IPageSource.CurrentPage
        {
            get
            {
                return this.PageSource.CurrentPage;
            }
        }

        #endregion

        #region Properties

        /// <summary>
        ///     Gets the ID of the <see cref="T:EPiServer.Core.PageData" /> instance bound to
        ///     the current template container.
        /// </summary>
        [Browsable(false)]
        private int BoundPageID
        {
            get
            {
                return (int)(this.ViewState["_boundPageID"] ?? 0);
            }
            set
            {
                this.ViewState["_boundPageID"] = value;
            }
        }

        /// <summary>
        ///     Gets or sets the <see cref="T:EPiServer.Web.ControlRenderContextBuilder" /> that should be used by the current
        ///     control instance.
        /// </summary>
        [Browsable(false)]
        private Injected<ControlRenderContextBuilder> ContextBuilder { get; set; }

        /// <summary>
        ///     Gets the current render context of this control instance.
        ///     This will not be available until the control has been added to the control tree.
        /// </summary>
        private PropertyContext CurrentContext
        {
            get
            {
                return this._currentContext
                       ?? (this._currentContext =
                           this.ContextBuilder.Service.BuildContext(this, this.PropertyName, this.BoundPageID));
            }
        }

        private RenderType LastRenderType
        {
            get
            {
                return (RenderType)(this.ViewState["_lastType"] ?? RenderType.Unknown);
            }
            set
            {
                this.ViewState["_lastType"] = value;
            }
        }

        /// <summary>
        ///     Gets or sets the object serializer to use when serializing to Json.
        /// </summary>
        /// <value>
        ///     The object serializer.
        /// </value>
        private Injected<IObjectSerializerFactory> ObjectSerializerFactory { get; set; }

        /// <summary>
        ///     Gets or sets the <see cref="P:EPiServer.Libraries.WebForms.Controls.Property.PropertyResolver" /> that should be
        ///     used by the current control instance.
        /// </summary>
        [Browsable(false)]
        private Injected<PropertyResolver> PropertyResolver { get; set; }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        ///     Binds a data source to the invoked server control and all its child controls.
        /// </summary>
        public override void DataBind()
        {
            try
            {
                this.OnDataBinding(EventArgs.Empty);
            }
            catch (NullReferenceException ex)
            {
                throw new NullReferenceException(
                    string.Format(
                        CultureInfo.InvariantCulture,
                        "A error occurred while databinding \"{0}\", some values set on the page where null references [{1}]",
                        this.UniqueID,
                        ex.Message));
            }

            this.SaveBoundState(this.CurrentContext);

            if (this._isDirty || this.LastRenderType != this.GetCurrentRenderType())
            {
                this.Controls.Clear();
                this.ClearChildViewState();
                this.ChildControlsCreated = true;
                this.CreateChildControls();
                this.TrackViewState();
            }

            this.EnsureChildControls();

            for (int index = 0; index < this.Controls.Count; ++index)
            {
                this.Controls[index].DataBind();
            }

            this._isBound = true;
        }

        /// <summary>
        ///     Triggers EnsureChildControls for the Property. Mainly used be control developers.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public void EnsurePropertyControlsCreated()
        {
            this.EnsureChildControls();
        }

        /// <summary>
        ///     Gets the specified content link.
        /// </summary>
        /// <typeparam name="T" />
        /// <param name="contentLink">The content link.</param>
        /// <returns />
        public T Get<T>(ContentReference contentLink) where T : IContentData
        {
            return this.ContentSource.Get<T>(contentLink);
        }

        /// <summary>
        ///     Gets the children.
        /// </summary>
        /// <typeparam name="T" />
        /// <param name="contentLink">The content link.</param>
        /// <returns />
        public IEnumerable<T> GetChildren<T>(ContentReference contentLink) where T : IContentData
        {
            return this.ContentSource.GetChildren<T>(contentLink);
        }

        #endregion

        #region Explicit Interface Methods

        /// <summary>
        /// Retrieve a <see cref="T:EPiServer.Core.PageData" /> listing
        /// </summary>
        /// <param name="pageLink">Reference to parent page</param>
        /// <returns>Returns a collection of pages directly below the page referenced by
        /// the <see cref="T:EPiServer.Core.PageReference" /> parameter.</returns>
        /// <example>
        /// The following code example demonstrates the usage of <b>GetChildren</b>.
        /// {D255958A-8513-4226-94B9-080D98F904A1}<code source="../CodeSamples/EPiServer/Core/IPageSourceSamples.cs" region="GetChildren" lang="cs" /></example>
        PageDataCollection IPageSource.GetChildren(PageReference pageLink)
        {
            return this.PageSource.GetChildren(pageLink);
        }

        /// <summary>
        /// Retrieves a <see cref="T:EPiServer.Core.PageData" /> object with information about a page, based on the
        /// <see cref="T:EPiServer.Core.PageReference" /> parameter.
        /// </summary>
        /// <param name="pageLink">Reference to the page being retrieved</param>
        /// <returns>PageData object requested</returns>
        /// <example>
        /// The following code example demonstrates how to get a start page.
        /// <code source="../CodeSamples/EPiServer/Core/IPageSourceSamples.cs" region="GetPage1" lang="cs" />
        /// The following code example demonstrates how to get a page by ID.
        /// {D255958A-8513-4226-94B9-080D98F904A1}<code source="../CodeSamples/EPiServer/Core/IPageSourceSamples.cs" region="GetPage2" lang="cs" /></example>
        PageData IPageSource.GetPage(PageReference pageLink)
        {
            return this.PageSource.GetPage(pageLink);
        }

        #endregion

        #region Methods

        /// <summary>
        ///     Gets the content source.
        /// </summary>
        /// <returns />
        internal IContentSource GetContentSource()
        {
            if (this._contentSource != null)
            {
                return this._contentSource;
            }

            for (Control parent = this.Parent;
                 parent != null && this._contentSource == null;
                 parent = parent.Parent)
            {
                this._contentSource = parent as IContentSource;
            }

            return this._contentSource ?? DataFactory.Instance;
        }

        /// <summary>
        ///     Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create
        ///     any child controls they contain in preparation for posting back or rendering.
        /// </summary>
        protected override void CreateChildControls()
        {
            if (this.InnerProperty == null || this.Page == null)
            {
                return;
            }

            RenderType currentRenderType = this.GetCurrentRenderType();

            if (this.Page.IsPostBack && this.LastRenderType != currentRenderType)
            {
                this.ClearChildViewState();
            }

            IPropertyControl propertyControl1 =
                PropertyControlClassFactory.Instance.CreatePropertyControl(
                    this.InnerProperty,
                    new HttpContextWrapper(HttpContext.Current),
                    this.Page,
                    this.RenderSettings.Tag);

            if (propertyControl1 != null)
            {
                this.Controls.Add((Control)propertyControl1);
                propertyControl1.PropertyData = this.InnerProperty;
                propertyControl1.RenderType = currentRenderType;
                propertyControl1.ValidationGroup = this.ValidationGroup;
                propertyControl1.Enabled = this.Enabled;
                PropertyDataControl propertyControl2 = propertyControl1 as PropertyDataControl;

                if (propertyControl2 != null)
                {
                    propertyControl2.AttributeSourceControl = this;
                    propertyControl2.CustomTagName = this.CustomTagName;
                    propertyControl2.CssClass = this.CssClass;
                    this.AssignCustomSettings(propertyControl2);
                }
                propertyControl1.SetupControl();
            }

            this.LastRenderType = currentRenderType;
            this._isDirty = false;
        }

        /// <summary>
        ///     Raises the <see cref="E:System.Web.UI.Control.PreRender" /> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs" /> object that contains the event data.</param>
        protected override void OnPreRender(EventArgs e)
        {
            if (this._isDirty || !PageReference.IsNullOrEmpty(this.PageLink) && !this._isBound)
            {
                this.DataBind();
            }

            this.EnsureChildControls();
            base.OnPreRender(e);
        }

        /// <summary>
        ///     Renders the control to the specified HTML writer.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter" /> object that receives the control content.</param>
        protected override void Render(HtmlTextWriter writer)
        {
            if (Global.IsDesignTime)
            {
                return;
            }

            if (this.InnerProperty != null)
            {
                this.EnsureChildControls();
                this.RenderChildren(writer);
            }
            else
            {
                this.RenderErrorMessage(writer);
            }
        }

        /// <summary>
        /// Allows editing.
        /// </summary>
        /// <returns><c>true</c> if editing is allowed, <c>false</c> otherwise.</returns>
        private bool AllowEditable()
        {
            PropertyData innerProperty = this.InnerProperty;
            PropertyContext currentContext = this.CurrentContext;

            if (innerProperty == null || innerProperty.IsDynamicProperty || currentContext == null)
            {
                return false;
            }

            ISecurable securable = currentContext.PropertyContainer as ISecurable
                                   ?? currentContext.CurrentContent as ISecurable;
            bool flag = this.CurrentContent != null
                        && this.CurrentContent.ContentLink.CompareToIgnoreWorkID(
                            currentContext.CurrentContent.ContentLink) && currentContext.IsEditable()
                        && !this.IsContainedInTemplate()
                        && (securable == null
                            || securable.GetSecurityDescriptor()
                                   .HasAccess(PrincipalInfo.CurrentPrincipal, AccessLevel.Edit));
            return flag;
        }

        /// <summary>
        /// Assigns the custom settings.
        /// </summary>
        /// <param name="propertyControl">The property control.</param>
        private void AssignCustomSettings(PropertyDataControl propertyControl)
        {
            propertyControl.EditorSettings = this.EditorSettings.Settings;
            propertyControl.RenderSettings = this.RenderSettings.Settings;
            propertyControl.OverlaySettings = this.OverlaySettings.Settings;
        }

        /// <summary>
        /// Gets the type of the current renderer.
        /// </summary>
        /// <returns>RenderType.</returns>
        private RenderType GetCurrentRenderType()
        {
            if (this.EditMode)
            {
                return RenderType.Edit;
            }

            if (!PageEditing.PageIsInEditMode || !this.Editable)
            {
                return RenderType.Default;
            }

            ContentRenderer enclosingContentRenderer = this.GetEnclosingContentRenderer();
            return enclosingContentRenderer != null ? enclosingContentRenderer.RenderType : RenderType.OnPageEdit;
        }

        /// <summary>
        /// Gets the enclosing content renderer.
        /// </summary>
        /// <returns>ContentRenderer.</returns>
        private ContentRenderer GetEnclosingContentRenderer()
        {
            for (Control parent = this.Parent; parent != null; parent = parent.Parent)
            {
                if (parent.GetType() == typeof(ContentRenderer))
                {
                    return parent as ContentRenderer;
                }
            }

            return null;
        }

        /// <summary>
        /// Gets the page source.
        /// </summary>
        /// <returns>IPageSource.</returns>
        private IPageSource GetPageSource()
        {
            return this.Page as IPageSource ?? DataFactory.Instance;
        }

        /// <summary>
        /// Initializes the inner fallback property.
        /// </summary>
        private void InitializeInnerFallbackProperty()
        {
            if (this._fallbackPropertyData != null || string.IsNullOrEmpty(this.FallbackPropertyName)
                || this.CurrentContext.PropertyContainer == null)
            {
                return;
            }

            this._fallbackPropertyData =
                this.PropertyResolver.Service.ResolveProperty(
                    this.CurrentContext.PropertyContainer.Property,
                    this.FallbackPropertyName,
                    this.PropertyScopeSeparator);
        }

        /// <summary>
        /// Initializes the inner property.
        /// </summary>
        private void InitializeInnerProperty()
        {
            if (this._propertyData != null || string.IsNullOrEmpty(this.PropertyName)
                || this.CurrentContext.PropertyContainer == null)
            {
                return;
            }

            this._propertyData =
                this.PropertyResolver.Service.ResolveProperty(
                    this.CurrentContext.PropertyContainer.Property,
                    this.PropertyName,
                    this.PropertyScopeSeparator);
        }

        /// <summary>
        ///     Checks if this property control is contained somewhere inside a templated control that fetches a
        ///     <see cref="T:EPiServer.Core.PageData" />.
        /// </summary>
        private bool IsContainedInTemplate()
        {
            for (Control control = this; control.Parent != null; control = control.Parent)
            {
                if (control.Parent is PageTemplateContainer
                    || control.Parent is IDataItemContainer && ((IDataItemContainer)control.Parent).DataItem is PageData)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        ///     Renders an error message to the specified HTML writer.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter" /> object that receives the control content.</param>
        /// <remarks>
        ///     This method is responsible for figuring out and render an error message.
        ///     It will look at the <see cref="P:EPiServer.Libraries.WebForms.Controls.Property.DisplayMissingMessage" /> to decide
        ///     if it should render certain errors.
        /// </remarks>
        private void RenderErrorMessage(HtmlTextWriter writer)
        {
            if (string.IsNullOrEmpty(this.PropertyName.Trim()))
            {
                writer.WriteLine("[Error: The PropertyName is not set.]");
            }
            else if (this.CurrentContext.PropertyContainer == null)
            {
                if (!this.DisplayMissingMessage)
                {
                    return;
                }

                writer.WriteLine(
                    "[Error: Property control for property '{0}' is contained in a page/control/template that does not contain any properties.]",
                    this.PropertyName);
            }
            else if (this.PropertyResolver.Service.ResolveProperty(
                this.CurrentContext.PropertyContainer.Property,
                this.PropertyName,
                this.PropertyScopeSeparator) == null)
            {
                if (!this.DisplayMissingMessage)
                {
                    return;
                }

                writer.WriteLine("[Error: Unable to find the property named '{0}'.]", this.PropertyName);
            }
            else
            {
                writer.WriteLine("[Error: Unknown error with property named '{0}'.]", this.PropertyName);
            }
        }

        /// <summary>
        /// Saves the bound state.
        /// </summary>
        /// <param name="context">The context.</param>
        private void SaveBoundState(PropertyContext context)
        {
            if (context == null || context.PropertyContainer == null
                || (context.PropertyContainer != context.CurrentContent
                    || !string.IsNullOrEmpty(context.CurrentContent.ContentLink.ProviderName)))
            {
                return;
            }

            this.BoundPageID = context.CurrentContent.ContentLink.ID;
        }

        #endregion
    }
}

So, what did I add/change?

I added a property

private PropertyData _fallbackPropertyData;

[Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("")]
        public string FallbackPropertyName
        {
            get
            {
                return
                    (string)
                    (this.ViewState["_fallbackPropertyName"]
                     ?? ((this._fallbackPropertyData != null) ? this._fallbackPropertyData.Name : string.Empty));
            }
            set
            {
                if (this.FallbackPropertyName == value)
                {
                    return;
                }

                this._fallbackPropertyData = null;
                this._isDirty = true;
                this.ViewState["_fallbackPropertyName"] = value;
            }
        }

I added a private to get the property data

 private void InitializeInnerFallbackProperty()
        {
            if (this._fallbackPropertyData != null || string.IsNullOrEmpty(this.FallbackPropertyName)
                || this.CurrentContext.PropertyContainer == null)
            {
                return;
            }

            this._fallbackPropertyData =
                this.PropertyResolver.Service.ResolveProperty(
                    this.CurrentContext.PropertyContainer.Property,
                    this.FallbackPropertyName,
                    this.PropertyScopeSeparator);
        }

And I changed the InnerProperty getter to use the fallback data if the main data is null

public PropertyData InnerProperty
        {
            get
            {
                this.InitializeInnerProperty();

                if (this._propertyData.Value != null)
                {
                    return this._propertyData;
                }

                this.InitializeInnerFallbackProperty();
                return this._fallbackPropertyData;
            }
            set
            {
                this.Editable = false;

                if (value != null)
                {
                    this.PropertyName = value.Name;
                }
                else
                {
                    this._currentContext = null;
                    this._isDirty = true;
                }

                this._propertyData = value;
            }
        }

You can also get the code on GitHub

Maybe it will of use to you too.

2 thoughts on “A property control with a fallback

  1. Another, much more simpler way is to have a global fallback, i.e. for all instances of that property:

    [PagePlugIn]
    public class PropertyFallbacks
    {
    private static Hashtable FallbackProperties { get; set; }

    public static void Initialize(int optionFlags)
    {
    PropertyDataCollection.GetHandler = PropertyFallbacks.DefaultPropertyHandler;
    PropertyFallbacks.FallbackProperties = new Hashtable { { “PageHeading”, “PageName” } };
    }

    public static PropertyData DefaultPropertyHandler(string name, PropertyDataCollection properties)
    {
    var data = properties.Get(name);

    if (data != null)
    {
    if (!data.IsNull || data.IsMetaData)
    {
    return data;
    }
    }

    if (PropertyFallbacks.FallbackProperties.ContainsKey(name))
    {
    data = PropertyFallbacks.DefaultPropertyHandler(PropertyFallbacks.FallbackProperties[name].ToString(), properties);

    if (data != null && data.Value != null)
    {
    return data;
    }
    }

    return DynamicPropertyCache.DynamicPropertyFinder.FindDynamicProperty(name, properties) ?? data;
    }
    }

    Like

    1. Thanks for sharing. Not sure about easier, as the control is of course reusable and you have a more fine grained option when and which property to use as a fallback.
      It doesn’t seem to work with the Property control within a web forms project though.

      Like

Leave a comment