Search code examples
asp.netcustom-server-controls

Custom server control sets properties to default on postback


By following a tutorial aimed at creating a video player I adapted it to build a HTML5 range control. Here is my code:

namespace CustomServerControls
{
    [DefaultProperty("Text")]
    [ToolboxData("<{0}:Range runat=server ID=Range1></{0}:Range>")]
    public class Range : WebControl
    {
        public int Min { get; set; }
        public int Max { get; set; }
        public int Step { get; set; }
        public int Value { get; set; }

        protected override void RenderContents(HtmlTextWriter output)
        {
            output.AddAttribute(HtmlTextWriterAttribute.Id, this.ID);
            output.AddAttribute(HtmlTextWriterAttribute.Width, this.Width.ToString());
            output.AddAttribute(HtmlTextWriterAttribute.Height, this.Height.ToString());

            if (Min > Max)
                throw new ArgumentOutOfRangeException("Min", "The Min value cannot be greater than the Max Value.");

            if (Value > Max || Value < Min)
                throw new ArgumentOutOfRangeException("Value", Value,
                                                      "The Value attribute can not be less than the Min value or greater than the Max value");

            if (Min != 0)
                output.AddAttribute("min", Min.ToString());

            if (Max != 0)
                output.AddAttribute("max", Max.ToString());

            if (Step != 0)
                output.AddAttribute("step", Step.ToString());

            output.AddAttribute("value", Value.ToString());

            output.AddAttribute("type", "range");
            output.RenderBeginTag("input");
            output.RenderEndTag();

            base.RenderContents(output);
        }
    }  
}

As you can see, very simple, and it works as far as being able to set the individual properties.

If I do a post back to check what the current value is, the control resets its properties back to default (0). I imagine this is a viewstate issue. Does anyone see anything i'm missing from the above code to make this work properly?

Edit:

I noticed this markup is being rendered to the page:

<span id="Range1" style="display:inline-block;">
    <input id="Range1" min="1" max="100" value="5" type="range">
</span>

Which is obviously wrong, I don't want a span tag created, and the input control doesn't have a name. So when I postback I get no data from the control.


Solution

  • Try this:

    [DefaultProperty("Value")]
    [ToolboxData("<{0}:Range runat=server />")]
    public class Range : WebControl, IPostBackDataHandler {
    
        private static readonly object mChangeEvent = new object();
    
        public Range() : base(HtmlTextWriterTag.Input) { }
    
        [Category("Events")]
        public event EventHandler Change {
            add { Events.AddHandler(mChangeEvent, value); }
            remove { Events.RemoveHandler(mChangeEvent, value); }
        }
    
        [DefaultValue(0)]
        public int Value {
            get { return (int?)ViewState["Value"] ?? 0; }
            set { ViewState["Value"] = value; }
        }
    
        protected override void AddAttributesToRender(HtmlTextWriter writer) {
            base.AddAttributesToRender(writer);
    
            // Add other attributes (Max, Step and value)
    
            writer.AddAttribute(HtmlTextWriterAttribute.Value, Value.ToString());
            writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "range");
        }
    
        protected virtual void OnChange(EventArgs e) {
            if (e == null) {
                throw new ArgumentNullException("e");
            }
    
            EventHandler handler = Events[mChangeEvent] as EventHandler;
    
            if (handler != null) {
                handler(this, e);
            }
        }
    
        #region [ IPostBackDataHandler Members ]
    
        public bool LoadPostData(string postDataKey, NameValueCollection postCollection) {
            int val;
    
            if (int.TryParse(postCollection[postDataKey], out val) && val != Value) {
                Value = val;
                return true;
            }
    
            return false;
        }
    
        public void RaisePostDataChangedEvent() {
            OnChange(EventArgs.Empty);
        }
    
        #endregion
    
    }
    

    As far as @VinayC said you have to use ViewState and ControlState (For critical data) to persist your control's states. Also, you have to implement IPostBackDataHandler to restore the last value and raise the change event as you can see in the preceding example.

    Update

    [DefaultProperty("Value")]
    [ToolboxData("<{0}:Range runat=server />")]
    public class Range : WebControl, IPostBackEventHandler, IPostBackDataHandler {
    
        [Category("Behavior")]
        [DefaultValue(false)]
        public bool AutoPostBack {
            get { return (bool?)ViewState["AutoPostBack"] ?? false; }
            set { ViewState["AutoPostBack"] = value; }
        }
    
        protected override void OnPreRender(EventArgs e) {
            base.OnPreRender(e);
    
            if (!DesignMode && AutoPostBack) {
    
                string script = @"
    var r = document.getElementById('{0}');
    
    r.addEventListener('mousedown', function (e) {{
        this.oldValue = this.value;
    }});
    
    r.addEventListener('mouseup', function (e) {{
        if (this.oldValue !== this.value) {{
            {1};
        }}
    }});";
    
                Page.ClientScript.RegisterStartupScript(
                    this.GetType(),
                    this.UniqueID,
                    string.Format(script, this.ClientID, Page.ClientScript.GetPostBackEventReference(new PostBackOptions(this))),
                    true);
            }
        }
    
        #region [ IPostBackEventHandler Members ]
    
        public void RaisePostBackEvent(string eventArgument) {
            // If you need to do somthing on postback, derive your control 
            // from IPostBackEventHandler interface.
        }
    
        #endregion
    
    }
    

    The above code illustrates how you can use ClientScriptManager.GetPostBackEventReference and IPostBackEventHandler to implement a simple AutoPostback for your Range control.