This question was originally about getting two-way binding to work at all, but due to lack of specific answers and otherwise progress along the way, I've been updating it - You can check the edit history, but I figured this is better for clarity.
The code listing below allows a single object to be two-way databound to a templated control. I'd like to extend this example in the simplest way possible to allow for nesting of similarly two-way databinding-enabled templated controls for complex-typed properties of the root-most object. For example, SampleFormData
has a property List<string> Items
. I'd like to be able to bind to this list within the root-most template (from this code listing), and either display the string data in an editable list of textboxes, perhaps, with commands for insert, delete, rebind-entered-changes (back to the bound object's List property). Also, if this were a list of a complex type (SampleFormChildData
, rather than string), a new embedded SampleSpecificEntryForm
could be used within the list, bound to each of the list's items, like a repeater. And so on down to the leaf-simple properties, if the author so chooses. The ui-fields need not be auto-generated, just available for binding.
Note: The case of the List<string>
is special because even the built-in bindings can't handle string as the DataItem directly - binding to strings directly as items in our list is not a requirement, but certainly valuable.
This is different from a FormView
because it is not built to expect to bind to one of a list of items, only to a single item as persisted in viewstate or where ever. Unlike the FormView, this only has a single default template akin to FormView's EditTemplate. Likewise, binding to a collection-like property would also only have one view - edit. There is no selection of the row and then editing. Everything is editable all the time. The purpose is to make two-way bound forms easier to build.
Seems to me that there ought to be two kinds of binding. SingleEntityBinding
and CollectionBinding
. SingleEntityBinding
takes a single object instance as a datasource (as prototyped by SampleSpecificEntryForm
) while CollectionBinding
could be bound to it's parent SingleEntityBinding
with attributes of DataSourceID="EntryForm1" DataMember="Items"
as in the code example for DataList1
below. Nesting of either type should be supported in either type. List manipulation such as insert/change/delete type operations against the backing-object's data are the responsibility of the form author; however, such mechanics would be relatively simple to implement.
Here's some code, hope it helps somebody. 200 points are out there for the best suggestions toward this laid-out goal...
using System.ComponentModel;
using System.Collections.Specialized;
using System.Collections.Generic;
namespace System.Web.UI.WebControls.Special
{
[Serializable]
public class SampleFormData
{
public string SampleString { get; set; }
public int SampleInt { get; set; }
public List<string> Items { get; set; }
public SampleFormData()
{
SampleString = "Sample String Data";
SampleInt = 5;
Items = new List<string>();
}
}
[ToolboxItem(false)]
public class SampleSpecificFormDataContainer : WebControl, INamingContainer, IDataItemContainer
{
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; }
}
[Bindable(true), Category("Data")]
public List<string> Items
{
get { return entryForm.FormData.Items; }
set { entryForm.FormData.Items = value; }
}
internal SampleSpecificFormDataContainer(SampleSpecificEntryForm entryForm)
{
this.entryForm = entryForm;
}
#region IDataItemContainer Members
public object DataItem { get { return entryForm.FormData; } }
public int DataItemIndex { get { return 0; } }
public int DisplayIndex { get { return 0; } }
#endregion
}
public class SampleSpecificEntryForm : DataBoundControl, INamingContainer, IDataSource
{
#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
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
{
SampleFormData data = ViewState["FormData"] as SampleFormData;
if (data == null)
{
data = new SampleFormData();
ViewState["FormData"] = data;
}
return data;
}
}
protected override void CreateChildControls()
{
if (!this.ChildControlsCreated)
{
this.ChildControlsCreated = true;
Controls.Clear();
formDataContainer = new SampleSpecificFormDataContainer(this);
Controls.Add(formDataContainer);
FormTemplate.InstantiateIn(formDataContainer);
}
}
protected override void PerformDataBinding(Collections.IEnumerable ignore)
{
CreateChildControls();
if (Page.IsPostBack)
{
//OrderedDictionary fields = new OrderedDictionary();
//ExtractValuesFromBindableControls(fields, formDataContainer); // Don't know what this would be for
foreach (System.Collections.DictionaryEntry entry in formTemplate.ExtractValues(formDataContainer))
{
if (((string)entry.Key).Equals("SampleString", StringComparison.Ordinal))
{
FormData.SampleString = (string)entry.Value;
}
if (((string)entry.Key).Equals("SampleInt", StringComparison.Ordinal))
{
int i;
if (int.TryParse((string)entry.Value, out i))
{
FormData.SampleInt = i;
}
}
}
}
formDataContainer.DataBind();
}
public SampleSpecificEntryForm()
{
this.PreRender += new EventHandler(SampleSpecificEntryForm_PreRender);
}
void SampleSpecificEntryForm_PreRender(object sender, EventArgs e)
{
SaveViewState();
}
#region IDataSource Members
public event EventHandler DataSourceChanged;
public DataSourceView GetView(string viewName)
{
return new PropertyView(this, viewName);
}
public Collections.ICollection GetViewNames()
{
return new List<string>() { "SampleString", "SampleInt", "Items" };
}
#endregion
}
// Not yet used ...
public class PropertyView : DataSourceView
{
SampleSpecificEntryForm owner;
string viewName;
protected override Collections.IEnumerable ExecuteSelect(DataSourceSelectArguments arguments)
{
if (viewName.Equals("SampleString", StringComparison.Ordinal))
{
return new object[] { owner.FormData.SampleString };
}
if (viewName.Equals("SampleInt", StringComparison.Ordinal))
{
return new object[] { owner.FormData.SampleInt };
}
if (viewName.Equals("Items", StringComparison.Ordinal))
{
return new object[] { owner.FormData.Items };
}
throw new InvalidOperationException();
}
public PropertyView(SampleSpecificEntryForm owner, string viewName)
: base(owner, viewName)
{
this.owner = owner;
this.viewName = viewName;
}
}
}
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="txtSampleString" runat="server" Text='<%# Bind("SampleString") %>'></asp:TextBox><br />
<asp:TextBox ID="txtSampleInt" runat="server" Text='<%# Bind("SampleInt") %>'></asp:TextBox><br />
<h3>
(<%# Container.SampleString %>, <%# Container.SampleInt %>) - aka -
(<%# DataBinder.Eval(Container, "SampleString")%>, <%# DataBinder.Eval(Container, "SampleInt")%>)</h3>
<br />
<asp:Button ID="btnUpdate" runat="server" Text="Update" /><br />
<br />
</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();
}
}
}
I've implemented IDataSource as well, in an attempt to be able to nest a list component like so (within the ):
<asp:DataList ID="DataList1" runat="server" DataSourceID="EntryForm1" DataMember="Items">
<EditItemTemplate>
<asp:TextBox ID="TextBox3" runat="server" Text="<%# Bind(".") %>"></asp:TextBox>
</EditItemTemplate>
<FooterTemplate>
<asp:Button ID="Button2" runat="server" Text="Add" CommandName="Add" />
</FooterTemplate>
</asp:DataList>
Any thoughts on how to make this work in a cascading way would be awesome (on the Items list property, for example). One of the challenges here is that Bind() can't refer to the databound object itself (a string in this case) but on a property of that item - making binding to a list awkward.
Thanks for any help!
Discoveries along the way
Implemented IDataItemContainer. I was very hopeful that this would fix it, but no. No noticable change. Oops, implemented it on the wrong class. Now it is Binding, but the values aren't being rebound to the bound object on postback. Hmmm...
As this article suggests, Page.GetDataItem() is the source of the exception. This exception is thrown if the page's _dataBindingContext is null or empty. The article does explain this, but it doesn't say how to ensure that the Page's _dataBindingContext is populated. I'll continue looking.
As the MSDN documentation says, DataBoundControl should implement PerformDataBinding instead of overriding DataBind(). I've done so and made both-way-binding work. Is this code necessary or should I be using something built-in?