The Muffin Man
The Muffin Man

Reputation: 20004

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.

Upvotes: 2

Views: 1446

Answers (2)

m3kh
m3kh

Reputation: 7941

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.

Upvotes: 1

VinayC
VinayC

Reputation: 49245

Issue is that your properties (Min, Max, Step, Value) are backed by instance fields - in ASP.NET, every page and every control instance on it gets re-created when page posts back. So every time, those properties will have default values (or values that you may set at design time) - in order to retain those values over post-back, you need to back your properties using view-state. For example:

public int Min 
{ 
   get
   {
       var value = ViewState["Min"];
       return null != value ? (int)value : 0;
   }
   set { ViewState["Min"] = value; }
}

Because view-state can be disabled, control authors may also use control state to back up their important properties (that are essential for control to work) - for using control state, you need override LoadControlState and SaveControlState methods.

For example (using control state):

public int Min { get; set; }
public int Max { get; set; }
public int Step { get; set; }
public int Value { get; set; }

protected override void OnInit(EventArgs e) 
{
    Page.RegisterRequiresControlState(this); // must for using control state
    base.OnInit(e);
}

protected override object SaveControlState() 
{
    return new int[] { Min, Max, Step, Value };
}

protected override void LoadControlState(object state) 
{
    var values = state as int[];
    if (null != values) 
    {
       Min = values[0]; Max = values[1];
       Step = values[2]; Value = values[3];
    }
}

Upvotes: 0

Related Questions