Two-way databinding of a custom templated control. Eval works, but not Bind.
- by Jason
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 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, http://msdn.microsoft.com/en-us/library/aa478964.aspx. 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. 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 : WebControl, 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!