Two-way databinding of a custom templated asp.net control

Posted by Jason on Stack Overflow See other posts from Stack Overflow or by Jason
Published on 2010-05-18T02:30:49Z Indexed on 2010/05/19 5:10 UTC
Read the original article Hit count: 389

I hate long code snippets and I'm sorry about this one, but it turns out that this asp.net stuff can't get much shorter and it's so specific that I haven't been able to generalize it without a full code listing.

I just want simple two-way, declarative, edit-only databinding to a single instance of an object. Not a list of objects of a type with a bunch of NotImplementedExceptions for Add, Delete, and Select, but just a single view-state persisted object. This is certainly something that can be done but I've struggled with an implementation for years. This newest, closest implementation was inspired by this article from 4-Guys-From-Rolla. Unfortunately, after implementing, I'm getting the following error and I don't know what I'm missing:

System.InvalidOperationException: Databinding methods such as Eval(), XPath(), and Bind() can only be used in the context of a databound control.

If I don't use Bind(), and only use Eval() functionality, it works. In that way, the error is especially confusing.

Update: Actually, using Eval() does NOT work, but using <%# Container.SampleString %> works. However, Eval("SampleString") gives the same error. That leads me back to this article I found earlier but had discarded. Now I believe it might be related, though I haven't cracked it yet ...

Here's the simplified codeset that still produces the error:

using System.ComponentModel;

namespace System.Web.UI.WebControls.Special
{
    public class SampleFormData
    {
        public string SampleString = "Sample String Data";
        public int SampleInt = -1;
    }

    [ToolboxItem(false)]
    public class SampleSpecificFormDataContainer : DataBoundControl, INamingContainer
    {
        SampleSpecificEntryForm entryForm;

        internal SampleSpecificEntryForm EntryForm
        {
            get { return entryForm; }
        }

        [Bindable(true), Category("Data")]
        public string SampleString
        {
            get { return entryForm.FormData.SampleString; }
            set { entryForm.FormData.SampleString = value; }
        }

        [Bindable(true), Category("Data")]
        public int SampleInt
        {
            get { return entryForm.FormData.SampleInt; }
            set { entryForm.FormData.SampleInt = value; }
        }

        internal SampleSpecificFormDataContainer(SampleSpecificEntryForm entryForm)
        {
            this.entryForm = entryForm;
        }
    }

    public class SampleSpecificEntryForm : WebControl, INamingContainer
    {
        #region Template
        private IBindableTemplate formTemplate = null;

        [Browsable(false), DefaultValue(null),
        TemplateContainer(typeof(SampleSpecificFormDataContainer), ComponentModel.BindingDirection.TwoWay),
        PersistenceMode(PersistenceMode.InnerProperty)]
        public virtual IBindableTemplate FormTemplate
        {
            get { return formTemplate; }
            set { formTemplate = value; }
        }
        #endregion

        #region Viewstate
        SampleFormData FormDataVS
        {
            get { return (ViewState["FormData"] as SampleFormData) ?? new SampleFormData(); }
            set { ViewState["FormData"] = value; SaveViewState(); }
        }
        #endregion

        public override ControlCollection Controls
        {
            get
            {
                EnsureChildControls();
                return base.Controls;
            }
        }

        private SampleSpecificFormDataContainer formDataContainer = null;

        [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public SampleSpecificFormDataContainer FormDataContainer
        {
            get
            {
                EnsureChildControls();
                return formDataContainer;
            }
        }

        [Bindable(true), Browsable(false)]
        public SampleFormData FormData
        {
            get { return FormDataVS; }
            set { FormDataVS = value; }
        }

        protected override void CreateChildControls()
        {
            if (!this.ChildControlsCreated)
            {
                Controls.Clear();
                formDataContainer = new SampleSpecificFormDataContainer(this);
                Controls.Add(formDataContainer);
                FormTemplate.InstantiateIn(formDataContainer);
                this.ChildControlsCreated = true;
            }
        }

        public override void DataBind()
        {
            CreateChildControls();
            base.DataBind();
        }
    }
}

With an ASP.NET page the following:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default2.aspx.cs" Inherits="EntryFormTest._Default2" EnableEventValidation="false" %>

<%@ Register Assembly="EntryForm" Namespace="System.Web.UI.WebControls.Special" TagPrefix="cc1" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>
        Welcome to ASP.NET!
    </h2>
    <cc1:SampleSpecificEntryForm ID="EntryForm1" runat="server">
        <FormTemplate>
            <asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("SampleString") %>'></asp:TextBox><br />
            <h3>(<%# Container.SampleString %>)</h3><br />
            <asp:Button ID="Button1" runat="server" Text="Button" />
        </FormTemplate>
    </cc1:SampleSpecificEntryForm>
</asp:Content>

Default2.aspx.cs

using System;

namespace EntryFormTest
{
    public partial class _Default2 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            EntryForm1.DataBind();
        }
    }
}

Thanks for any help!

© Stack Overflow or respective owner

Related posts about ASP.NET

Related posts about 2-way-object-databinding